In a React + TypeScript codebase, how do you type a function component's props, and why do most teams avoid React.FC?
answer
- annotate the parameter, infer the return
- FC's children perk was removed
- ReactNode is wide, JSX.Element is one element
- derive native props, don't retype them
- types disappear at runtime
basics
~20 sType the props argument directly: function Button({ label }: ButtonProps). React.FC adds nothing you need, forces the return type, and since @types/react 18 no longer implies children, so teams declare children explicitly with React.ReactNode instead.
solid answer
~50 sThe house style is to annotate the parameter and let TypeScript infer the return: `type ButtonProps = { label: string; onSelect?: (id: string) => void }` and then `function Button({ label, onSelect }: ButtonProps)`. That is all the typing a component needs. `React.FC` is avoided because it buys nothing — it does not check props any better, it constrains the return type in ways that get in the way of components returning arrays or strings, and its one historical selling point, an implicit `children` prop, was removed in `@types/react` 18. So a component using `FC` still has to declare `children?: React.ReactNode` explicitly, which is the thing people used `FC` to avoid. When wrapping a native element, extend its props with `React.ComponentPropsWithoutRef<'button'>` rather than retyping every attribute, and use `React.ReactNode` for anything renderable rather than `JSX.Element`, which excludes strings, null and arrays.
code
typescript · 20 linestype UserCardProps = {
name: string;
role?: 'admin' | 'member';
onSelect?: (name: string) => void;
children?: React.ReactNode;
};
function UserCard({ name, role = 'member', onSelect, children }: UserCardProps) {
return (
<article onClick={() => onSelect?.(name)}>
<h2>{name}</h2>
<p>{role}</p>
{children}
</article>
);
}
type IconButtonProps = Omit<React.ComponentPropsWithoutRef<'button'>, 'size'> & {
size?: 'sm' | 'lg';
};go deeper
Be able to declare a props type and annotate the parameter, mark optional props with ?, and type children as React.ReactNode when the component renders whatever it is given.
Explain why FC adds nothing, that its implicit children were removed in @types/react 18, and how ComponentPropsWithoutRef derives a native element's full attribute surface instead of retyping it.
Show how you would design a library's public prop types — omitting colliding native members, choosing ReactNode versus ReactElement deliberately, and keeping the compile-time contract separate from runtime validation of untrusted data.
Own the codebase-wide stance: whether FC is banned by lint, how shared prop types are versioned for consumers, and where schema validation lives so typed components are never the last line of defence.
## The default shape A React component in TypeScript needs exactly one annotation: the type of its single argument. ```typescript type UserCardProps = { name: string; role?: 'admin' | 'member'; onSelect?: (id: string) => void; children?: React.ReactNode; }; function UserCard({ name, role = 'member', onSelect, children }: UserCardProps) { return ( <article onClick={() => onSelect?.(name)}> <h2>{name}</h2> <p>{role}</p> {children} </article> ); } ``` The return type is inferred and is correct without help. Optional props are marked with `?` and given destructuring defaults, which narrows them to non-optional inside the body. `interface` and `type` are both fine; `interface` is declaration-mergeable, which matters when you publish a library and want consumers to be able to augment it, and `type` handles unions and intersections more naturally. ## Why React.FC fell out of favour `React.FC<Props>` types the whole function rather than its parameter: ```typescript const UserCard: React.FC<UserCardProps> = ({ name }) => <h2>{name}</h2>; ``` Four objections, in order of how often they actually bite: 1. **It does not add checking.** Annotating the parameter already gives you full prop checking at every call site. `FC` gives the same errors, just spelled differently. 2. **It fixes the return type.** `FC` declares a specific return type, which fights components that legitimately return a string, a number, or an array of elements. Inference handles those without ceremony. 3. **Its selling point is gone.** Before `@types/react` 18, `FC` silently added `children?: ReactNode` to your props. That was the reason people used it. It was also a bug factory — every component accepted children whether or not it rendered them, so passing children to a component that ignored them type-checked fine. React 18's types removed it, so `FC` no longer saves you the `children` declaration. 4. **It reads worse with generics.** A generic component (`function List<T>({ items }: ListProps<T>)`) is straightforward as a function declaration and awkward as a typed const. The honest summary in an interview: `FC` is not dangerous, it is just an extra indirection whose original benefit was removed. Existing code using it is fine; new code has no reason to reach for it. ## Typing children and anything renderable Use `React.ReactNode` for "whatever can be rendered". It covers elements, strings, numbers, `null`, `undefined`, booleans and arrays of those. `JSX.Element` (or `React.ReactElement`) is much narrower — a single element — so typing `children: JSX.Element` rejects `<Card>hello</Card>` and rejects two children. Reach for the narrower type deliberately, when the component genuinely requires a single element it will clone or inspect. For a prop that is a component rather than rendered output, `React.ComponentType<P>` types "a function or class component taking P", and `React.ElementType` types "anything usable as a tag", which is what an `as` prop needs. ## Extending a native element's props Retyping `onClick`, `disabled`, `aria-label` and the rest by hand is both tedious and wrong within a release. Derive them: ```typescript type ButtonProps = React.ComponentPropsWithoutRef<'button'> & { variant?: 'primary' | 'ghost'; }; ``` `ComponentProps<'button'>` gives the element's props including `ref`; `ComponentPropsWithoutRef<'button'>` omits it, which is usually what you want for a rest object. `ComponentProps<typeof SomeComponent>` does the same trick for another component, extracting its prop type without exporting it separately. If your own prop clashes with a native one — a `size` union where the native attribute is a `number` — intersecting produces an impossible type. Omit the native member first: `Omit<React.ComponentPropsWithoutRef<'button'>, 'size'> & { size?: 'sm' | 'lg' }`. ## Refs in React 19 Because `ref` is a regular prop on function components in React 19, a component that forwards one can simply declare it: ```typescript type InputProps = React.ComponentPropsWithoutRef<'input'> & { ref?: React.Ref<HTMLInputElement>; }; ``` No `forwardRef` generic gymnastics are needed for the common case. `forwardRef` still exists and older code that uses it still type-checks. ## Runtime versus compile time One more thing worth saying out loud: TypeScript prop types vanish at build time, and React 19 removed runtime `propTypes` checking for function components. So neither mechanism validates data arriving from a network response or a CMS at runtime. If a component's props are shaped by untrusted input, validate that input with a schema at the data boundary and let the component's types describe the already-validated shape. Conflating the two is the mistake behind "but it is typed, so it cannot be undefined".
- When would you deliberately type children as React.ReactElement rather than React.ReactNode?When the component does something to the child rather than just rendering it — inspecting its props, cloning it, or requiring exactly one element to attach a ref to. ReactElement enforces that contract at compile time. For an ordinary slot that just renders whatever it is given, ReactNode is correct, because it also admits strings, numbers, arrays and null.
- How do you extend a native button's props when one of your own prop names collides with a native attribute?Omit the native member before intersecting: `Omit<React.ComponentPropsWithoutRef<'button'>, 'size'> & { size?: 'sm' | 'lg' }`. A plain intersection would try to satisfy both the native type and yours at once, which for conflicting types produces something no value can satisfy — the error usually surfaces at the call site rather than the declaration.
- If the props are typed, do you still need runtime validation anywhere?Yes, wherever the data crosses a boundary the compiler cannot see — API responses, CMS payloads, URL parameters, local storage. TypeScript erases at build time and React 19 no longer runs propTypes for function components, so nothing checks those values at runtime. Validate with a schema at the data layer and let the component types describe the validated shape.
saying these in an interview costs you the question
- Believing React.FC gives stronger prop checking
- Assuming FC still adds children implicitly
- Typing children as JSX.Element so strings are rejected
- Retyping every native attribute by hand on a wrapper
- Treating TypeScript types as runtime prop validation