Lesson 71 of 80
The three patterns that actually come up when typing real components: discriminated unions for variant props, PropsWithChildren, and generic components.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A React component is a function, so it takes the exact same TypeScript typing tools any function does — a props type is just a parameter type. What makes component props distinct in practice is three recurring shapes: props that come in mutually exclusive variants, props that accept children, and components generic over the type of data they're given. Get comfortable with those three and you can type almost anything you'll actually write.
PropsWithChildren — the built-in helper that adds a typed children field to whatever props shape you already have:
import type { PropsWithChildren } from "react";
type PanelProps = PropsWithChildren<{ title: string }>;
// equivalent to: { title: string; children?: React.ReactNode }
function Panel({ title, children }: PanelProps) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}
Discriminated unions for props that only make sense in specific combinations. A Button that's either a link or a button-that-does-something, never both:
type ButtonProps =
| { variant: "link"; href: string }
| { variant: "button"; onClick: () => void };
function Button(props: ButtonProps) {
if (props.variant === "link") {
return <a href={props.href}>Go</a>; // props.onClick doesn't exist here — TS knows it
}
return <button onClick={props.onClick}>Go</button>; // and props.href doesn't exist here
}
The variant field is the discriminant — a literal value TypeScript can check to narrow which branch of the union you're in. Inside each if branch, TypeScript knows exactly which shape you have and disallows the other shape's fields. A plain object with both href? and onClick? marked optional would not give you this — it would let someone pass both or neither, and you'd be back to runtime checks.
Generic components for a component whose prop types depend on the data passed in:
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
};
function List<T>({ items, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={i}>{renderItem(item)}</li>
))}
</ul>
);
}
// T is inferred as User here — renderItem's parameter is typed as User automatically
<List items={users} renderItem={(u) => u.name} />;
Nobody passes <User> explicitly — TypeScript infers T from the items array, the same way it infers a generic function's type parameter from its arguments.
React.FC<Props> is no longer the recommended default. It used to implicitly add children to every component's props whether or not it accepted any, and it makes generic components awkward to type. Plain function Component(props: Props) is the current idiomatic style — children is opt-in via PropsWithChildren only when actually needed.{ href?: string; onClick?: () => void } compiles for any combination, including both or neither — it can't express "exactly one of these, and which one determines what else is valid." Only a union with a shared literal discriminant does that.T gets inferred. If you've typed a generic utility function before, <List> above is the identical mechanism wearing a component's clothes.None of this is special "React typing" machinery — a component is a function, props is its parameter, and TypeScript's discriminated unions and generics work on it exactly as they would on any other function. The only genuinely React-specific piece is PropsWithChildren, and even that is just a small utility type adding one optional field — not a different type system.