TypeScript Generics That Keep UI Components Honest

Author

Amit Verma

Date Published

Install Node.js using NVM

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";
2
3type 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">;
8
9export 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.

Need a fast project partner? Let’s chat on WhatsApp.
Hire me on WhatsApp