React 19 combined with TypeScript offers powerful patterns for building type-safe components. Here are the most useful patterns that improve developer experience and catch bugs at compile time.
Discriminated Unions
Discriminated unions let you model component props where certain combinations of props are valid while others are not. This prevents impossible states at the type level.
type ButtonProps =
| { variant: 'primary'; icon?: never }
| { variant: 'icon'; icon: React.ReactNode };
function Button(props: ButtonProps) {
if (props.variant === 'icon') {
return <button>{props.icon}</button>;
}
return <button className="primary">Click me</button>;
}With this pattern, TypeScript will error if you try to pass an icon prop when variant is "primary", eliminating a whole class of runtime bugs.
Generic Components
Generic components preserve type information through the component boundary, making reusable components fully type-safe.
The List Pattern
A generic list component infers the item type from the items array and carries it through to renderItem:
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
};
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}Polymorphic Components
Polymorphic components render as different HTML elements while maintaining proper type safety for each element's props.
The "as" Prop Pattern
By using constrained generics, the component accepts props specific to whichever element it renders as:
type AsProps<C extends React.ElementType> = {
as?: C;
} & React.ComponentPropsWithoutRef<C>;
function Text<C extends React.ElementType = 'span'>({
as,
...props
}: AsProps<C>) {
const Component = as || 'span';
return <Component {...props} />;
}
// Usage:
<Text as="h1">Title</Text> // renders <h1>
<Text as="a" href="/about">Link</Text> // renders <a>Key Takeaways
These patterns share a common theme: leveraging TypeScript's type system to make invalid states unrepresentable. Discriminated unions prevent impossible prop combinations, generics preserve type flow, and polymorphic components ensure element-specific prop safety.