Typing React
- Props are typed with a
typeor aninterfaceon the props parameter.childrenisReactNode. A plain function is the usual form;React.FCworks but adds nothing. - Hooks infer types from the initial value; a type argument is needed when
the initial value is narrower than the state:
useState<User | null>(null). - Events have their own types:
ChangeEvent<HTMLInputElement>,MouseEvent<HTMLButtonElement>. Inline handlers get them inferred. ComponentProps<'button'>gives all props of a native element, for wrapper components. In React 19refis a regular prop, soforwardRefis no longer needed.
Applies to: React 19.3, @types/react 19.3, TypeScript 7.0.
Details
Props and Children
| What | Type |
|---|---|
| anything React can render | ReactNode |
| exactly one element | ReactElement |
| a component as a prop | ComponentType<Props> |
| a render prop | (item: T) => ReactNode |
| inline styles | CSSProperties |
| props of a native element | ComponentProps<'input'> |
| props of a component | ComponentProps<typeof Button> |
props plus children | PropsWithChildren<Props> |
- The return type of a component is usually inferred. If written, it is
ReactNode: since TypeScript 5.1 a component may return a string or an array, not only an element ornull.React.JSX.Elementis narrower. - The global
JSXnamespace is gone in@types/react19: writeReact.JSX. React.FC<Props>does not addchildrensince@types/react18 and can't declare a generic component. Most code uses a plain function with typed props.- Optional props with defaults are set in destructuring:
{ size = 'm' }: { size?: Size }. See Props. - Props that depend on each other, such as
variant: 'link'requiringhref, are a discriminated union.
Events
| Event | Type |
|---|---|
onChange of an input | ChangeEvent<HTMLInputElement> |
onClick | MouseEvent<HTMLButtonElement> |
onKeyDown | KeyboardEvent<HTMLInputElement> |
onSubmit of a form | SubmitEvent<HTMLFormElement> |
onInput | InputEvent<HTMLInputElement> |
onFocus, onBlur | FocusEvent<HTMLInputElement> |
| a handler type for a prop | ChangeEventHandler<HTMLInputElement> and so on |
- These are React's synthetic events, imported from
react, not the DOMMouseEvent. FormEventis deprecated (since@types/react19.2.10): useSubmitEvent,ChangeEventorInputEventfor what the event really is.e.currentTargetis the element the handler is attached to and has its type.e.targetmay be a nested element; it is typed precisely only for change events of form fields.
Hooks
useState:useState(0)isnumber. An empty array is inferred asnever[]andnullasnull, so write the type:useState<string[]>([]),useState<User | null>(null).useRef: for a DOM element,useRef<HTMLInputElement>(null); for a mutable value,useRef<number | null>(null). In React 19 the argument is required andcurrentis always writable (MutableRefObjectis deprecated).useReducer: type the reducer function and its actions as a discriminated union, then let the hook infer the rest. Passing type arguments (useReducer<State, Action>) does not compile in React 19 types: the second parameter is now the tuple of action arguments.- Custom hooks that return a tuple need
as const, otherwise the result is an array of a union:return [value, setValue] as const. - Context:
createContext<Value | null>(null)and a custom hook that throws when the value isnull, so consumers getValuewithout checks.
Wrapping Native Elements
- Extend
ComponentProps<'button'>to accept every native attribute, includingaria-*,disabledandref. ComponentPropsWithoutRef<'button'>excludesref, for components that must not accept one.- Generic components (a
List<T>or aSelect<T>) are described in Generics.
Example
import {
useRef,
useState,
type ChangeEvent,
type ComponentProps,
type ReactNode,
} from 'react';
// A wrapper around a native element; ref is a regular prop in React 19
type ButtonProps = ComponentProps<'button'> & {
variant?: 'primary' | 'secondary';
};
function Button({ variant = 'primary', className, ...props }: ButtonProps) {
return <button className={`${variant} ${className ?? ''}`} {...props} />;
}
// Children and events
function Field({ label, children }: { label: string; children: ReactNode }) {
return (
<label>
{label} {children}
</label>
);
}
function NameForm() {
const [name, setName] = useState('');
const [tags, setTags] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setName(e.target.value);
}
return (
<form
onSubmit={(e) => {
e.preventDefault(); // e: SubmitEvent<HTMLFormElement>, inferred
setTags([...tags, name]);
inputRef.current?.focus();
}}
>
<Field label="Name">
<input ref={inputRef} value={name} onChange={handleChange} />
</Field>
<Button type="submit">Add</Button>
</form>
);
}
useReducer and context
import { createContext, use, useReducer, type ReactNode } from 'react';
type State = { count: number };
type Action = { type: 'increment' } | { type: 'set'; value: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'set':
return { count: action.value }; // narrowed: value exists here
}
}
type CounterContextValue = {
state: State;
dispatch: (action: Action) => void;
};
const CounterContext = createContext<CounterContextValue | null>(null);
function useCounter() {
const value = use(CounterContext);
if (!value) throw new Error('useCounter must be used inside CounterProvider');
return value; // CounterContextValue, no null
}
function CounterProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(reducer, { count: 0 }); // inferred
return (
<CounterContext value={{ state, dispatch }}>{children}</CounterContext>
);
}
function Counter() {
const { state, dispatch } = useCounter();
dispatch({ type: 'set' }); // Error: Argument of type '{ type: "set"; }' is not assignable to parameter of type 'Action'.
return (
<button onClick={() => dispatch({ type: 'increment' })}>
{state.count}
</button>
);
}
Follow-up Questions
React.FCor a plain function? A plain function: it supports generics, andFCgives nothing extra since it stopped addingchildren.- How do you type a component that accepts every prop of
<input>?ComponentProps<'input'>, extended with an intersection orOmitfor props that change meaning. - How do you get the props type of a third-party component that does not export
it?
ComponentProps<typeof Component>. - Why does
useState([])cause errors later? It is inferred asnever[], so nothing can be added. - Why is
forwardRefnot needed anymore? In React 19 function components getrefas a regular prop.
Common Mistakes
children: React.JSX.Elementinstead ofReactNode: strings, numbers and arrays are rejected.- Importing
MouseEventfrom the DOM types instead ofreact. - Typing context as
createContext<Value>({} as Value): a missing provider is then a runtime error that the compiler can't see. - Writing the event type by hand everywhere instead of letting inline handlers infer it.
useRef<HTMLDivElement>()withoutnull: it does not compile in React 19 types.