Props Patterns
When to Use
When defining the public API of a component. Well-designed props prevent breaking changes and communicate intent clearly.
Decision
| If you need... | Use... | Why |
|---|---|---|
| Multiple exclusive modes (link vs button) | Discriminated union | TypeScript enforces valid combinations; no impossible states |
| Component that renders as any element | Polymorphic as + ComponentPropsWithoutRef |
Caller chooses element; correct HTML semantics |
| Pass unknown extra props to the DOM | Spread ...rest onto the root element |
Allows data-*, aria-*, event handlers without explicit declarations |
| Optional props with sensible defaults | TypeScript optional + default parameters | Self-documenting; no prop proliferation |
| Required prop that has no safe default | Required (no ?, no default) |
Fails at build time, not runtime |
Pattern
Discriminated union (button vs link):
type ButtonAsButton = { as?: 'button' } & React.ButtonHTMLAttributes<HTMLButtonElement>;
type ButtonAsAnchor = { as: 'a' } & React.AnchorHTMLAttributes<HTMLAnchorElement>;
type ButtonProps = (ButtonAsButton | ButtonAsAnchor) & { variant?: 'primary' | 'ghost' };
export function Button({ as: As = 'button', variant = 'primary', ...props }: ButtonProps) {
return <As className={cn(buttonVariants({ variant }))} {...props} />;
}
// Usage: <Button as="a" href="/page">Link</Button> — TypeScript enforces href only on 'a'
Polymorphic component (generic):
type PolymorphicProps<T extends React.ElementType> = {
as?: T;
} & React.ComponentPropsWithoutRef<T>;
export function Text<T extends React.ElementType = 'p'>({ as, className, ...props }: PolymorphicProps<T>) {
const Component = as ?? 'p';
return <Component className={cn('text-base', className)} {...props} />;
}
// Usage: <Text as="h2">Heading</Text> — inherits h2 props; no unnecessary divs
Common Mistakes
- Accepting
classNamebut not spreading...rest→ callers can't addaria-*ordata-*without explicit prop declarations - Using a generic
options: objectprop → kills TypeScript inference; use explicit union types - Defaulting optional boolean props to
true→<Button disabled>looks like it takes no value; prefer explicitdisabled={false}defaults or don't add a default - Over-restricting props to only what you need today → design systems evolve; spread
...restfor extensibility - Forgetting that
classNamemerging needscn()→ naive string concatenation breaks Tailwind class precedence