TypeScript and React are a powerful combination, but getting the most out of both requires knowing the right patterns.
1. Discriminated Unions for Props
Instead of optional props with complex conditional logic:
// ❌ Avoid this
type ButtonProps = {
variant: "primary" | "secondary";
icon?: React.ReactNode;
iconPosition?: "left" | "right";
};
// ✅ Use discriminated unions
type ButtonProps =
| { variant: "primary"; icon: React.ReactNode; iconPosition: "left" | "right" }
| { variant: "secondary"; icon?: never; iconPosition?: never };
2. Polymorphic Components with as Prop
type PolymorphicProps<T extends React.ElementType> = {
as?: T;
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<T>;
function Box<T extends React.ElementType = "div">({
as,
children,
...props
}: PolymorphicProps<T>) {
const Component = as || "div";
return <Component {...props}>{children}</Component>;
}
// Usage
<Box as="a" href="/about">Link styled as box</Box>
<Box as="button" onClick={handleClick}>Button box</Box>
3. Const Assertions for Configuration
const ROUTES = {
home: "/",
blog: "/blog",
about: "/about",
} as const;
type Route = (typeof ROUTES)[keyof typeof ROUTES];
// Type: "/" | "/blog" | "/about"
4. Extract Props from Components
type InputProps = React.ComponentProps<"input">;
type ButtonProps = React.ComponentProps<typeof Button>;
5. Generic Components
interface 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>
);
}
These patterns will make your React components more robust and your developer experience significantly better. TypeScript isn't just about catching bugs — it's about designing better APIs.