TypeScript Generics That Keep UI Components Honest
Author
Amit Verma
Date Published

During a design review last month our design lead asked why a button component accepted five different prop shapes. The answer—poorly scoped generics—was a red flag I have seen too often.
I now treat TypeScript generics as promises: each generic must map to a real constraint and the component should be usable without reading its implementation.
When I build a reusable UI primitive I start with a concrete prop object, then introduce a single generic type parameter if a consumer truly needs to extend it.
• Give generics meaningful defaults so Storybook examples compile without extra typing ceremony.
• Expose helper types like ButtonProps<T> to keep the public API obvious.
1type ButtonTone = "primary" | "ghost";23type ButtonProps<T extends React.ElementType = "button"> = {4 as?: T;5 tone?: ButtonTone;6 onPress?: React.ComponentPropsWithoutRef<T>["onClick"];7} & Omit<React.ComponentPropsWithoutRef<T>, "as" | "onClick">;89export function Button<T extends React.ElementType = "button">(10 props: ButtonProps<T>11) {12 const { as: Component = "button", tone = "primary", onPress, ...rest } = props;13 return <Component data-tone={tone} onClick={onPress} {...rest} />;14}
This pattern lets our marketing pages swap anchor tags in without losing type safety while keeping the default button path clean.
I wire these components into Chromatic visual tests so we notice when type-level assumptions diverge from rendered output.
If the generic signature ever grows longer than the component body I split the work into a typed wrapper and a minimal view. The split keeps the code review focused on behavior instead of angle-bracket puzzles.