Booklet TypeScript in React: A Quick Reference
- Booklet TypeScript in React: A Quick Reference
This booklet will guide you through using TypeScript in React, starting with the essentials and moving into more advanced patterns. By the end, you'll have a solid reference for building robust and type-safe React applications.
Introduction: Why TypeScript in React?
TypeScript is a superset of JavaScript that adds static typing to the language. When integrated with React, it provides numerous benefits:
- Type Safety: Catch type-related errors at compile time rather than at runtime, leading to fewer bugs in production.
- Improved Code Readability and Maintainability: Explicit types make it easier to understand the shape of data and function signatures, especially in large codebases and team environments.
- Enhanced Developer Experience: Better IDE support (autocompletion, refactoring, immediate error feedback) significantly boosts productivity.
- Easier Refactoring: The compiler can immediately identify parts of your application that might break after a change, making refactoring safer and more efficient.
Essential Aspects of Using TypeScript in React
1. Project Setup
To start a new React project with TypeScript, the easiest way is to use Create React App:
npx create-react-app my-ts-app --template typescript
cd my-ts-app
npm startFor an existing React project, you can add TypeScript by installing the necessary packages:
npm install --save-dev typescript @types/react @types/react-dom @types/nodeYou'll also need a tsconfig.json file to configure TypeScript. Create React App sets this up for you. For manual setup, ensure you have settings like jsx: "react-jsx" (or react), lib (including dom), and ideally strict: true.
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true, // Enable strict type-checking options
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx", // or "react" for older versions
"moduleResolution": "node",
"baseUrl": "src" // Example: allows absolute imports from 'src'
},
"include": ["src"]
}2. Typing Component Props
This is the most fundamental aspect of using TypeScript in React. You define an interface or type alias to describe the shape of your component's props.
// src/components/Greeting.tsx
import React from "react";
// Define an interface for the component's props
interface GreetingProps {
name: string;
age?: number; // Optional prop
isActive: boolean;
}
// Functional Component with props type annotation
const Greeting: React.FC<GreetingProps> = ({ name, age, isActive }) => {
return (
<div>
<h1>Hello, {name}!</h1>
{age && <p>You are {age} years old.</p>}
<p>Status: {isActive ? "Active" : "Inactive"}</p>
</div>
);
};
export default Greeting;
// Usage example in App.tsx
// import Greeting from './components/Greeting';
// <Greeting name="Alice" isActive={true} />
// <Greeting name="Bob" age={30} isActive={false} />Key Points:
interfacevs.type: Both can be used for defining prop types.interfaceis generally preferred for object shapes as it can be extended and implemented, whiletypealiases are more versatile for unions, intersections, and primitive types. For consistency, many prefertypefor component props.React.FC(Functional Component): While widely used,React.FCimplicitly provideschildrenprop anddisplayName. For more explicit control and to avoid some issues with default props or generics, some prefer typing props directly on the function:tsx// Alternative for functional component typing const Greeting = ({ name, age, isActive }: GreetingProps) => { // ... };Both approaches are valid; choose what feels more comfortable for your team.
3. Typing State with useState
useState can often infer the type of your state based on the initial value. However, for more complex types, especially when the initial state is null or an empty object that will later contain data, explicit typing is crucial.
// src/components/Counter.tsx
import React, { useState } from "react";
interface User {
id: number;
name: string;
email: string;
}
const Counter: React.FC = () => {
// Type inference: `count` is inferred as number
const [count, setCount] = useState(0);
// Explicit type for an object state that might be null initially
const [user, setUser] = useState<User | null>(null);
// Explicit type for an array of objects
const [items, setItems] = useState<string[]>([]);
const increment = () => setCount((prevCount) => prevCount + 1);
const loadUser = () => {
// Simulate fetching user data
setTimeout(() => {
setUser({ id: 1, name: "John Doe", email: "john.doe@example.com" });
}, 1000);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
{user ? (
<div>
<p>
User: {user.name} ({user.email})
</p>
</div>
) : (
<button onClick={loadUser}>Load User</button>
)}
{items.length === 0 && <p>No items yet.</p>}
</div>
);
};
export default Counter;4. Typing Event Handlers
React's synthetic event types are available in @types/react. You'll typically use React.MouseEvent, React.ChangeEvent, React.FormEvent, etc., and specify the HTML element type they originate from.
// src/components/Form.tsx
import React, { useState } from "react";
const Form: React.FC = () => {
const [inputValue, setInputValue] = useState<string>("");
// Typing for an input change event
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};
// Typing for a button click event
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log("Button clicked!", event.currentTarget.tagName);
};
// Typing for a form submission event
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // Prevent default form submission behavior
console.log("Form submitted with value:", inputValue);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={handleChange}
placeholder="Type something..."
/>
<button type="submit" onClick={handleClick}>
Submit
</button>
</form>
);
};
export default Form;5. Typing Children (React.ReactNode)
When a component accepts children, you typically type the children prop as React.ReactNode. This type is a union of all possible things React can render (JSX elements, strings, numbers, booleans, null, undefined).
// src/components/Card.tsx
import React from "react";
interface CardProps {
title: string;
children: React.ReactNode; // Defines that this component accepts children
footer?: React.ReactNode; // Optional footer content
}
const Card: React.FC<CardProps> = ({ title, children, footer }) => {
return (
<div style={`{ border: "1px solid #ccc", padding: "16px", margin: "16px" }`}>
<h2>{title}</h2>
<div>{children}</div>
{footer && (
<div
style={`{
marginTop: "10px",
borderTop: "1px dashed #eee",
paddingTop: "10px",
}`}
>
{footer}
</div>
)}
</div>
);
};
export default Card;
// Usage example
// import Card from './components/Card';
// <Card title="My Awesome Card">
// <p>This is the content of the card.</p>
// <ul>
// <li>Item 1</li>
// <li>Item 2</li>
// </ul>
// </Card>
// <Card title="Another Card" footer={<span>© 2025</span>}>
// <p>With a footer!</p>
// </Card>6. Strict Mode in tsconfig.json
Enabling strict: true in your tsconfig.json is highly recommended. It turns on a suite of stricter type-checking options that catch common programming errors and improve overall code quality.
Advanced Patterns with TypeScript in React
1. Custom Hooks
Custom hooks are functions that allow you to reuse stateful logic across components. Typing them involves specifying the types of their arguments and return values.
// src/hooks/useFetch.ts
import { useState, useEffect } from "react";
interface FetchResult<T> {
data: T | null;
loading: boolean;
error: string | null;
}
// Generic custom hook to fetch data
function useFetch<T>(url: string): FetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result: T = await response.json();
setData(result);
} catch (err: any) {
// Type 'any' can be narrowed down if error structure is known
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
export default useFetch;Usage example in a component:
import useFetch from "../hooks/useFetch";
interface Post {
id: number;
title: string;
body: string;
}
const PostsList: React.FC = () => {
const {
data: posts,
loading,
error,
} = useFetch<Post[]>("https://jsonplaceholder.typicode.com/posts");
if (loading) return <div>Loading posts...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>Posts</h1>
<ul>
{posts?.map((post) => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
};Using as const for Tuples:
When a custom hook returns an array, TypeScript might infer a union type ((boolean | typeof load)[]) instead of a tuple ([boolean, typeof load]). as const can fix this.
// src/hooks/useToggle.ts
import { useState, useCallback } from "react";
// Using `as const` to ensure a tuple type is inferred
function useToggle(initialState: boolean = false) {
const [state, setState] = useState(initialState);
const toggle = useCallback(() => setState((prev) => !prev), []);
return [state, toggle] as const;
}
export default useToggle;Usage:
import useToggle from "./hooks/useToggle";
const MyComponent: React.FC = () => {
const [isOn, toggle] = useToggle(false); // isOn is boolean, toggle is () => void
return <button onClick={toggle}>{isOn ? "On" : "Off"}</button>;
};2. Context API
Typing React Context involves creating an interface for your context value and providing an initial value that matches that type (often null with a runtime check).
// src/context/ThemeContext.tsx
import React, { createContext, useState, useContext, ReactNode } from "react";
// 1. Define the shape of your context value
interface ThemeContextType {
theme: "light" | "dark";
toggleTheme: () => void;
}
// 2. Create the context with an initial value (can be null, but requires handling)
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// 3. Create a provider component
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
const [theme, setTheme] = useState<"light" | "dark">("light");
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
};
const contextValue: ThemeContextType = {
theme,
toggleTheme,
};
return (
<ThemeContext.Provider value={contextValue}>
{children}
</ThemeContext.Provider>
);
};
// 4. Create a custom hook to consume the context
export const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
};
// Usage example
// src/App.tsx
// import { ThemeProvider } from './context/ThemeContext';
// import ThemeSwitcher from './components/ThemeSwitcher';
// const App: React.FC = () => {
// return (
// <ThemeProvider>
// <ThemeSwitcher />
// {/* Other components that need theme */}
// </ThemeProvider>
// );
// };
// src/components/ThemeSwitcher.tsx
// import React from 'react';
// import { useTheme } from '../context/ThemeContext';
// const ThemeSwitcher: React.FC = () => {
// const { theme, toggleTheme } = useTheme();
// return (
// <button onClick={toggleTheme}>
// Switch to {theme === 'light' ? 'dark' : 'light'} theme
// </button>
// );
// };3. Higher-Order Components (HOCs)
// src/hocs/withLoading.tsx
import React, { ComponentType } from "react";
// Define the props that the HOC will inject
interface WithLoadingProps {
isLoading: boolean;
}
// A generic HOC that adds a loading state
function withLoading<P extends object>(
WrappedComponent: ComponentType<P & WithLoadingProps>
) {
// Create a display name for better debugging in React Dev Tools
const displayName =
WrappedComponent.displayName || WrappedComponent.name || "Component";
// Return a new functional component that wraps the original
const ComponentWithLoading: React.FC<P> = (props) => {
// In a real scenario, isLoading would come from state/props of the HOC's parent
// or internal logic. For this example, we'll just hardcode it.
const isLoading = true; // Example: This would come from some HOC-specific logic
if (isLoading) {
return <div>Loading...</div>;
}
// Spread both original props and injected props (using type assertion if needed)
return <WrappedComponent {...(props as P)} isLoading={isLoading} />;
};
ComponentWithLoading.displayName = `withLoading(${displayName})`;
return ComponentWithLoading;
}
export default withLoading;
// Usage example
// src/components/MyDataComponent.tsx
// import React from 'react';
// interface MyDataComponentProps {
// data: string;
// isLoading: boolean; // This prop will be injected by the HOC
// }
// const MyDataComponent: React.FC<MyDataComponentProps> = ({ data, isLoading }) => {
// if (isLoading) return <div>Loading data inside component...</div>;
// return <div>Data: {data}</div>;
// };
// src/App.tsx
// import withLoading from './hocs/withLoading';
// import MyDataComponent from './components/MyDataComponent';
// const LoadedDataComponent = withLoading(MyDataComponent);
// const App: React.FC = () => {
// return (
// <LoadedDataComponent data="Hello World" />
// );
// };Explanation of HOC Typing:
P extends object: This is a generic type parameter representing the original props of theWrappedComponent. We useobjectas a general constraint.ComponentType<P & WithLoadingProps>: TheWrappedComponentexpects its original props (P) combined with the props injected by the HOC (WithLoadingProps). The&(intersection type) merges these two types.React.FC<P>: The returned componentComponentWithLoadingonly accepts theP(original) props from its consumer, asWithLoadingPropsare handled internally by the HOC.
4. Render Props
Render props is a pattern where a component takes a function as a prop, and that function returns a React element to be rendered. This allows the consumer to control the rendering logic.
// src/components/DataFetcher.tsx
import React, { useState, useEffect, ReactNode } from "react";
// Define the interface for the data that will be fetched
interface UserData {
id: number;
name: string;
}
// Define the props for the DataFetcher component
interface DataFetcherProps {
// The `render` prop is a function that receives the fetched data
// and returns a ReactNode (JSX element, string, etc.)
render: (data: UserData[]) => ReactNode;
url: string;
}
const DataFetcher: React.FC<DataFetcherProps> = ({ render, url }) => {
const [data, setData] = useState<UserData[] | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result: UserData[] = await response.json();
setData(result);
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
if (loading) return <div>Loading data...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return null; // Or some fallback UI
// Call the render prop with the fetched data
return <>{render(data)}</>;
};
export default DataFetcher;
// Usage example
// src/App.tsx
// import React from 'react';
// import DataFetcher from './components/DataFetcher';
// const App: React.FC = () => {
// return (
// <DataFetcher
// url="https://jsonplaceholder.typicode.com/users"
// render={(users) => (
// <div>
// <h2>User List (Render Prop)</h2>
// <ul>
// {users.map((user) => (
// <li key={user.id}>{user.name}</li>
// ))}
// </ul>
// </div>
// )}
// />
// );
// };5. Utility Types for Components
TypeScript provides several built-in utility types that are incredibly useful when working with React components.
Partial<T>: Makes all properties inToptional. Useful for default props or draft objects.tsxinterface UserProfile { name: string; email: string; age?: number; } // A component that can take partial user data for editing const UserForm: React.FC<{ initialData: Partial<UserProfile> }> = ({ initialData }) => { const [formData, setFormData] = useState<Partial<UserProfile>>(initialData); // ... form logic return (/* ... */); }; // Usage: <UserForm initialData={{ name: 'Jane' }} />Pick<T, K>: Constructs a type by picking the set of propertiesKfromT.tsxinterface Product { id: string; name: string; price: number; description: string; imageUrl: string; } // Component only needs name and price type ProductCardProps = Pick<Product, "name" | "price">; const ProductCard: React.FC<ProductCardProps> = ({ name, price }) => { return ( <div> <h3>{name}</h3> <p>${price.toFixed(2)}</p> </div> ); };Omit<T, K>: Constructs a type by omitting the set of propertiesKfromT. Useful when extending a type but excluding certain properties.tsx// Assuming ButtonHTMLAttributes provides all standard button props type CustomButtonProps = Omit< React.ComponentPropsWithoutRef<"button">, "className" | "children" > & { variant: "primary" | "secondary"; label: string; // Add any other custom props here }; const CustomButton: React.FC<CustomButtonProps> = ({ variant, label, ...rest }) => { const className = variant === "primary" ? "btn-primary" : "btn-secondary"; return ( <button className={className} {...rest}> {label} </button> ); };Required<T>: Makes all properties inTrequired.tsxinterface Config { apiKey?: string; endpoint?: string; } // Function that requires all config properties to be present function initializeApp(config: Required<Config>) { console.log(config.apiKey, config.endpoint); // Guaranteed to be present }React.ComponentProps<T>/React.ComponentPropsWithoutRef<T>/React.ComponentPropsWithRef<T>: These are powerful for "mirroring" props of existing HTML elements or React components.React.ComponentProps<'div'>would give you all props for a<div>element.React.ComponentProps<typeof MyCustomComponent>would give you the props forMyCustomComponent.ComponentPropsWithoutRefandComponentPropsWithRefare more explicit about whether therefprop is included.tsx// Example: A wrapper around an HTML button type ButtonWrapperProps = React.ComponentPropsWithoutRef<"button"> & { customLabel: string; }; const ButtonWrapper: React.FC<ButtonWrapperProps> = ({ customLabel, ...rest }) => { return <button {...rest}>{customLabel}</button>; }; // Usage: <ButtonWrapper customLabel="Click Me" onClick={() => alert('Clicked!')} type="submit" />
6. Polymorphic Components (The as Prop)
Polymorphic components are components that can render as different HTML elements or React components based on a prop (commonly named as or component). Typing these requires a good understanding of generics and utility types.
// src/components/PolymorphicButton.tsx
import React from "react";
// 1. Define the base props that are always present
type BaseProps = {
children: React.ReactNode;
};
// 2. Define a default element type
type PolymorphicButtonProps<E extends React.ElementType = "button"> =
BaseProps &
// Merge the intrinsic element props, omitting base props
Omit<React.ComponentPropsWithoutRef<E>, keyof BaseProps> & {
as?: E; // The 'as' prop itself
};
// 3. Implement the polymorphic component
function PolymorphicButton<E extends React.ElementType = "button">({
as,
children,
...rest
}: PolymorphicButtonProps<E>) {
const Component = as || "button";
return <Component {...rest}>{children}</Component>;
}
export default PolymorphicButton;
// Usage examples:
// import PolymorphicButton from './components/PolymorphicButton';
// Renders as a default button
// <PolymorphicButton onClick={() => alert('Button Clicked')}>
// Default Button
// </PolymorphicButton>
// Renders as an anchor tag (<a>)
// <PolymorphicButton as="a" href="https://example.com" target="_blank">
// Link Button
// </PolymorphicButton>
// Renders as a div
// <PolymorphicButton as="div" style={{ padding: '10px', background: 'lightblue' }}>
// Div Button
// </PolymorphicButton>
// Renders as a custom component (e.g., from a UI library)
// const MyCustomLinkComponent = ({ to, children }: { to: string; children: React.ReactNode }) => (
// <a href={to}>{children}</a>
// );
// <PolymorphicButton as={MyCustomLinkComponent} to="/dashboard">
// Custom Link Button
// </PolymorphicButton>Explanation of Polymorphic Component Typing:
E extends React.ElementType = 'button': This generic type parameter captures the element type (e.g.,'a','div',typeof MyCustomComponent). It defaults to'button'.Omit<React.ComponentPropsWithoutRef<E>, keyof BaseProps>: This is the core. It takes all the props of the selected elementE(e.g.,<a>props ifEis'a') and then removes any properties that are already defined in ourBasePropsto prevent conflicts.- The
asprop itself is added asas?: E;.
Best Practices and Tips
- Enable Strict Mode: Always start with
strict: truein yourtsconfig.json. It catches many common errors. - Avoid
any: While convenient,anydefeats the purpose of TypeScript. Try to be as specific as possible. If you truly don't know the type, considerunknownand then narrow it down with type guards. - Organize Your Types: For larger projects, consider a
typesdirectory or separate type definition files (e.g.,your-component.d.tsortypes.ts). - Leverage Type Inference: Let TypeScript infer types when it can, especially for simple
useStatecalls or function return types. Don't over-annotate. - Use Interfaces for Objects, Types for Unions/Intersections: This is a common convention, though both can often achieve similar results.
- Read React TypeScript Cheatsheets: This is an invaluable resource for various React-TypeScript patterns and common questions.
- ESLint with TypeScript: Integrate ESLint with TypeScript support to enforce consistent coding styles and catch potential issues early.
- Keep
@types/*packages updated: Ensure your@types/reactand@types/react-dompackages match your React version.
This booklet should serve as a solid foundation and quick reference for effectively using TypeScript in your React projects. Remember that consistent application of these patterns will lead to more robust, maintainable, and enjoyable code. Happy coding!