Concept
The beginner framing: React and TypeScript combine constantly in real codebases, and most of what's needed is a direct application of concepts already covered, typing props is just typing a function parameter; typing a hook's return value is ordinary type inference. A handful of React-specific patterns do need dedicated treatment, though.
Typing props and children
interface ButtonProps {
label: string;
onClick: () => void;
children?: React.ReactNode; // the standard type for "anything React can render"
}
function Button({ label, onClick, children }: ButtonProps) {
return <button onClick={onClick}>{label}{children}</button>;
}children is typed as React.ReactNode, the broadest type covering everything React can actually render (elements, strings, numbers, arrays of those, null, undefined, booleans). Marking it optional (children?:) is standard, since not every component that could receive children always does.
Typing event handlers
function SearchInput() {
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value); // e.target correctly typed as HTMLInputElement, not a generic EventTarget
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
console.log(e.clientX, e.clientY);
}
return (
<>
<input onChange={handleChange} />
<button onClick={handleClick}>Go</button>
</>
);
}React's synthetic event types (React.ChangeEvent<T>, React.MouseEvent<T>, and others) are generic over the specific DOM element type, parameterizing with HTMLInputElement versus HTMLButtonElement means e.target is correctly typed as that specific element, not a generic, less-useful EventTarget.
Typing hooks
const [count, setCount] = useState(0); // inferred: number
const [user, setUser] = useState<User | null>(null); // explicit, inference alone would give `null`, too narrow
function useCounter(initial: number) {
const [count, setCount] = useState(initial);
const increment = () => setCount((c) => c + 1);
return { count, increment }; // return type INFERRED as { count: number; increment: () => void }
useState's type parameter is usually inferable from the initial value, but when the initial value's inferred type would be narrower than what the state will actually hold over time (like starting null but expecting a User object later), an explicit type argument is necessary, since inference alone has nothing but that initial null to go on.
React 19: ref as a regular prop
interface InputProps {
ref?: React.Ref<HTMLInputElement>;
placeholder?: string;
}
function MyInput({ ref, placeholder }: InputProps) {
return <input ref={ref} placeholder={placeholder} />;
}Confirmed by compiling this exact pattern against this app's installed React 19.2.4 and matching @types/react: as of React 19, ref can be declared as a regular prop on a plain function component's props interface, no forwardRef wrapper required, a genuine simplification over the pattern most existing React+TypeScript tutorials still teach, which predates this change.
Generic components: the trailing-comma requirement
// ❌ fails to parse in a .tsx file:
const List = <T>({ items }: { items: T[] }) => { /* ... */ };
// ✅ the fix, note the trailing comma after T:
const List = <T,>({ items, renderItem }: { items: T[]; renderItem: (item: T) => React.ReactNode }) => {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li
Confirmed by compiling both forms: <T> at the start of an arrow function in a .tsx file is genuinely ambiguous with JSX syntax, TypeScript's parser reads it as the opening of a JSX element named T, producing a cascade of confusing syntax errors ("JSX element 'T' has no corresponding closing tag" and several more downstream). The fix, <T,>, is real, necessary syntax, the trailing comma has no semantic meaning of its own; it exists purely to disambiguate "this is a generic type parameter list" from "this is the start of a JSX tag," resolving the parser ambiguity. (This specific ambiguity only affects arrow function components in .tsx files, a regular function List<T>(...) declaration doesn't have this problem, since function unambiguously signals a function declaration, not JSX.)
Try It
Predict the outcome before checking the solution.
const Wrapper = <T>({ value }: { value: T }) => <div>{String(value)}</div>;Solution
This fails to compile with a cascade of confusing syntax errors, starting with something like "JSX element 'T' has no corresponding closing tag", confirmed directly. TypeScript's .tsx parser reads <T> at this position as the start of a JSX element, not a generic type parameter list, since both use the same <Name> syntax and the parser needs additional information to disambiguate them. The fix is adding a trailing comma: const Wrapper = <T,>({ value }: { value: T }) => <div>{String(value)}</div>;, real, required syntax specifically for this case, not a stylistic choice.
Implement It Yourself
Build a small generic component using the correct syntax, combining several of this topic's patterns together:
interface SelectProps<T> {
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (option: T) => string;
}
function Select<T>({ options, value, onChange, getLabel }: SelectProps<T>) {
function handleChange(e: React.ChangeEvent<HTMLSelectElement>) {
Using a function Select<T> declaration (rather than an arrow function) sidesteps the trailing-comma requirement entirely, since function already unambiguously signals a function declaration to the .tsx parser, worth knowing as an alternative to the <T,> arrow-function fix, not just a limitation to work around.
Under the Hood
Everything here builds directly on prior topics rather than introducing new type-system concepts: props typing is TypeScript Basics's structural typing applied to a function's single parameter object; generic components are Generics's type parameter mechanism, unchanged, just applied to a component function; and hook return-type inference is the same inference-first behavior covered from the very first topic in this domain. React+TypeScript isn't a separate type system, it's the same TypeScript, applied to React's specific APIs and JSX syntax.
Common Mistakes
1. Forgetting the trailing comma on a generic arrow-function component
const List = <T>(props: { items: T[] }) => null; // ❌ parsed as JSX, cascading errorsThis is confirmed to be a genuine parser ambiguity, not a rare edge case, any generic arrow-function component in a .tsx file needs either the trailing comma or an alternative like <T extends unknown>.
2. Typing children as React.ReactElement instead of React.ReactNode
interface Props { children: React.ReactElement; } // ❌ too narrow, rejects strings, arrays, null, etc.React.ReactElement only covers actual JSX elements, it rejects perfectly valid children like plain text or fragments. React.ReactNode is the correct, broad type for "anything React can render" in the general case.
3. Assuming forwardRef is still required for every component that accepts a ref
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => { /* ... */ }); // still valid, but no longer REQUIREDConfirmed working as of React 19: ref can now be a plain prop on a regular function component, forwardRef still works for backward compatibility, but a lot of existing tutorials present it as the only way, which is no longer accurate.
Best Practices
- Type
childrenasReact.ReactNodeby default, reserving the narrowerReact.ReactElementonly for the rare case where literally just a single JSX element (not text, arrays, or fragments) is genuinely required. - Parameterize event types with the specific element (
React.ChangeEvent<HTMLInputElement>, not a bareReact.ChangeEvent) to get correctly-typed access toe.target's specific properties. - Prefer
function Component<T>(...)overconst Component = <T,>(...) =>when writing a generic component, if the trailing-comma requirement feels error-prone or unfamiliar to the team, both work identically, but thefunctionform sidesteps the ambiguity entirely.
Performance Tips
- None of this, prop typing, event typing, generic components, has any runtime performance implication; it's all compile-time-only, consistent with TypeScript's general type-erasure model applied to React code exactly like any other code.
- Using
React.ReactNodeversus a narrower children type has no runtime cost difference either, the choice is purely about how precisely the type system models what's actually allowed, not about any runtime behavior.
