Every question in this free section, with its full answer — open one to check yourself, or use the card view above to drill them in order.
You can define a type or interface for your props, and then use it directly in the component signature.
type GreetProps = {
name: string;
};
function Greet({ name }: GreetProps) {
return <p>Hello, {name}!</p>;
}
export default Greet;This ensures that name must be a string, which helps prevent runtime errors.
Interfaces and type aliases are similar but have some differences. Interfaces are often recommended for component props because they can be extended and merged.
interface ButtonProps {
label: string;
onClick: () => void;
}
// Alternatively, a type alias:
type ButtonPropsAlias = {
label: string;
onClick: () => void;
};
function Button({ label, onClick }: ButtonProps) {
return <button onClick={onClick}>{label}</button>;
}Interfaces can be augmented in multiple declarations, while type aliases shine in advanced scenarios (like unions). Most often, interfaces are a solid default for describing props.
React provides event types like React.MouseEvent and React.ChangeEvent. You should also specify the HTML element type for e.target.
function TextInput() {
const [value, setValue] = React.useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
return <input value={value} onChange={handleChange} />;
}Using the explicit event type ensures proper type safety for event properties like e.target.value.
You can pass a generic type to useState when TypeScript can’t infer it, or if you need more complex typings like unions.
const [count, setCount] = React.useState<number>(0);
type CountOrNull = number | null;
const [maybeCount, setMaybeCount] = React.useState<CountOrNull>(null);Often, TypeScript infers the type from the initial value. But explicit types are helpful when your state can be multiple types or starts off empty (e.g., null).
You define a state type and an action type, often as a discriminated union. Then pass these types to the reducer function.
type CounterState = { count: number };
type CounterAction =
| { type: "increment"; amount: number }
| { type: "decrement"; amount: number };
function counterReducer(state: CounterState, action: CounterAction): CounterState {
switch (action.type) {
case "increment":
return { count: state.count + action.amount };
case "decrement":
return { count: state.count - action.amount };
default:
return state;
}
}
const [state, dispatch] = React.useReducer(counterReducer, { count: 0 });This way, TypeScript enforces that all actions in the reducer match a valid shape, improving type safety and readability.
Generics enable you to create flexible components, often used for lists, tables, or form controls.
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
};
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}This makes List reusable for any data type while preserving full type information for the renderItem function.
Custom hooks can accept typed parameters and return typed values, often leveraging generics to represent data shapes.
function useFetch<T>(url: string): { data: T | null; loading: boolean } {
const [data, setData] = React.useState<T | null>(null);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
setLoading(true);
fetch(url)
.then((res) => res.json())
.then((json: T) => setData(json))
.finally(() => setLoading(false));
}, [url]);
return { data, loading };
}
// Usage:
type User = { id: number; name: string };
const { data: user, loading } = useFetch<User>("/api/user/1");Providing T to the hook ensures the returned data is always the correct shape, minimizing type casts.
When using useRef with a DOM element, pass the element type as a generic to ensure current is typed correctly.
function TextInput() {
const inputRef = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}
// React 19: ref is a regular prop — no forwardRef needed
type MyInputProps = {
placeholder?: string;
ref?: React.Ref<HTMLInputElement>;
};
function MyInput({ placeholder, ref }: MyInputProps) {
return <input ref={ref} placeholder={placeholder} />;
}
// Pre-React 19 you'd wrap it in forwardRef:
// const MyInput = React.forwardRef<HTMLInputElement, MyInputProps>(
// ({ placeholder }, ref) => <input ref={ref} placeholder={placeholder} />
// );In React 19, ref is just another prop, so you type it as React.Ref<HTMLInputElement> on your props. forwardRef still works but is now legacy; either way, specify the ref’s element type to keep strict typing.
Use React.ReactNode for most children, as it covers strings, numbers, elements, portals, fragments, etc.
type CardProps = {
children: React.ReactNode;
};
function Card({ children }: CardProps) {
return <div className="card">{children}</div>;
}Use React.ReactElement if you specifically want a single valid element instead of arbitrary content.
Utility types let you modify existing prop types to create subsets, make properties optional, or omit them, etc.
interface FullProps {
title: string;
subtitle: string;
onClick?: () => void;
}
// Using Pick to select certain props:
type TitleProps = Pick<FullProps, "title">;
function Title({ title }: TitleProps) {
return <h2>{title}</h2>;
}
// Using Omit to remove certain props:
type NoSubtitle = Omit<FullProps, "subtitle">;These utilities are invaluable when reusing prop structures across multiple components with slight variations.
You define a context value type, then provide a default value that matches that type in React.createContext.
type AuthContextValue = {
user: { name: string } | null;
login: () => void;
};
const AuthContext = React.createContext<AuthContextValue>({
user: null,
login: () => {},
});
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = React.useState<{ name: string } | null>(null);
const login = () => setUser({ name: "Alice" });
return (
<AuthContext.Provider value={{ user, login }}>
{children}
</AuthContext.Provider>
);
}Then in consumers, call const { user, login } = React.useContext(AuthContext). TypeScript will correctly infer the types for user and login.
Discriminated unions let you define multiple possible prop shapes that differ by a 'discriminant' property, ensuring TypeScript can narrow types accurately.
type ShapeProps =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function ShapeDisplay(props: ShapeProps) {
if (props.kind === "circle") {
return <div>Circle radius: {props.radius}</div>;
}
return <div>Square side: {props.side}</div>;
}TS narrows props by kind. It enforces that you handle all branches (circle, square), preventing missing case errors.
Intersection types (&) merge multiple type definitions. This is useful when combining multiple sets of props that must all be satisfied.
type StyleProps = {
color?: string;
};
type ClickProps = {
onClick: () => void;
};
type ButtonProps = StyleProps & ClickProps;
function StyledButton({ color, onClick }: ButtonProps) {
return (
<button style={{ color }} onClick={onClick}>
Click me
</button>
);
}Here, StyledButton requires both StyleProps and ClickProps, making it a more flexible, composable component.
You can use constraints like T extends object or K extends keyof T to build generic components that are type-safe for different data shapes.
function Table<T extends object, K extends keyof T>({
data,
columns,
}: {
data: T[];
columns: K[];
}) {
return (
<table>
<thead>
<tr>
{columns.map((col) => <th key={String(col)}>{String(col)}</th>)}
</tr>
</thead>
<tbody>
{data.map((row, idx) => (
<tr key={idx}>
{columns.map((col) => <td key={String(col)}>{String(row[col])}</td>)}
</tr>
))}
</tbody>
</table>
);
}The generic constraints ensure columns can only include valid keys of T, preventing invalid column references and making the component highly reusable.
When using React.lazy, the resulting component is typically typed as React.LazyExoticComponent. Ensure that any props the lazy component accepts are exported and typed.
const LazyComponent = React.lazy(() => import("./SomeComponent"));
function App() {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</React.Suspense>
);
}The fallback can be any React.ReactNode. If SomeComponent has props, pass them normally, ensuring your import statement references the correctly typed component.