My Second Article

Thoughts on building design systems with TypeScript

Building a Design System with TypeScript

Design systems are the backbone of scalable frontend development. When paired with TypeScript, they become even more powerful — giving you autocomplete, type safety, and self-documenting APIs across every component.

Why TypeScript in a Design System?

A design system without types is a design system that relies on documentation nobody reads. TypeScript turns your component API into the documentation.

interface ButtonProps {
  variant: 'primary' | 'secondary' | 'ghost';
  size: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  onClick?: () => void;
}

With this interface, consumers can't accidentally pass variant="danger" if it doesn't exist — the compiler catches it first.

Tokens as Constants

Design tokens — colors, spacing, typography — should live as typed constants rather than raw strings scattered through stylesheets.

export const colors = {
  amber: '#C9A84C',
  void: '#080808',
  inkMuted: 'rgba(240, 237, 230, 0.55)',
} as const;

export type Color = keyof typeof colors;

The as const assertion narrows the type to exact string literals, so colors.amber is typed as '#C9A84C', not just string.

Component Variants with CVA

Class Variance Authority (CVA) is a lightweight library that pairs well with this approach. It lets you define component variants declaratively:

const button = cva('btn-base', {
  variants: {
    variant: {
      primary: 'btn-primary',
      ghost: 'btn-ghost',
    },
    size: {
      sm: 'btn-sm',
      md: 'btn-md',
    },
  },
});

The returned function is fully typed — the variant keys are inferred directly from the config.

Closing Thoughts

A well-typed design system pays dividends every time a new developer joins your team or you revisit code six months later. The upfront investment in types and tokens is small compared to the long-term reduction in bugs and inconsistency.