A typed prop is a small treaty between the component author and every caller. The compiler is the only neutral party. Honor the treaty and your IDE becomes a second pair of eyes.
The four ways to type props
Inline object type — fastest, most local. Good for small components.
Named type alias — exported, reusable, composable with intersection (&) and union (|).
Named interface — like type, but extensible via declaration merging. Use for prop shapes you expect third parties to extend.
Inferred from a function signature — for HOC-style helpers that wrap a component.
In practice, named type aliases win. They're flexible (unions, intersections, mapped types all work), and they don't accidentally collide via declaration merging.
The children prop
Almost every layout component takes children. The canonical type is React.ReactNode — covers strings, numbers, JSX, fragments, arrays, null, undefined, and booleans. Don't try to narrow it unless you have a real reason (e.g. 'only accept a single string'); you'll just fight legitimate uses.
The native-element extension pattern
A common shape: your component renders a <button> with extra behavior, and you want callers to be able to pass any native button prop (onClick, aria-label, disabled, etc.) without you re-declaring each one. The TS-native way: extend React.ComponentPropsWithoutRef<'button'>.
Generics in components
Generic components are how you build a typed <List items={users} renderItem={(u) => ...} /> where TypeScript infers that u is a User from the type of users. The syntax has a quirk in .tsx files (the type parameter <T> looks like JSX), which you'll see in the code block.
Type the contract, not the implementation. Your props type describes what callers must pass and what they can expect. It is the public face. Internal state, refs, derived values — those don't belong in the props type. Keeping the boundary clean keeps refactors local.
Code
The four patterns side-by-side·tsx
// 1. Inline
function Hello1({ name }: { name: string }) {
return <p>Hello, {name}</p>;
}
// 2. Named type alias (the workhorse)
type Hello2Props = {
name: string;
count?: number; // optional
};
export function Hello2({ name, count = 0 }: Hello2Props) {
return <p>Hello, {name} (×{count})</p>;
}
// 3. Interface (declaration-merging friendly)
export interface Hello3Props {
name: string;
}
export function Hello3({ name }: Hello3Props) {
return <p>Hello, {name}</p>;
}
// 4. Inferred — wrap an existing component
export function withSilly<P>(Inner: React.ComponentType<P>) {
return function Sillied(props: P) {
return <Inner {...props} />;
};
}
Native-element extension — forward unknown props to the underlying button·tsx
Build a typed Input component that extends React.ComponentPropsWithoutRef<'input'> and adds two props: label: string and error?: string. Render a <label> wrapping the input with the error (when present) shown in red below. Use the Input component three ways: (1) plain text input, (2) with onChange + value, (3) with type='email' + aria-label. Confirm TypeScript accepts all native input props without you listing them.
Hint
Spread the rest props onto the inner <input>. Use ...rest after destructuring out the props you handle yourself (label, error, possibly className).
Progress
Progress is local-only — sign in to sync across devices.