Props Patterns
When to Use
Use these patterns 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
- Wrong: Accepting
classNamebut not spreading...rest→ Right: Always spread...restso callers can addaria-*anddata-* - Wrong: Using a generic
options: objectprop → Right: Use explicit union types;objectkills TypeScript inference - Wrong: Defaulting optional boolean props to
true→ Right: Prefer explicitdisabled={false}defaults or omit the default - Wrong: Over-restricting props to only what you need today → Right: Spread
...restfor extensibility; design systems evolve - Wrong: Naive string concatenation for
className→ Right: Always merge withcn()to preserve Tailwind class precedence