Here's a typical design system component:
This compiles but also accepts "bleu-500", "text-blue-500" (the full class instead of just the token), "hotpink", and "". Not what we want in our design system.
Template literal types
TypeScript 4.1 introduced template literal types: the ability to construct union types from string combinations using the same backtick syntax as JavaScript. For example:
TypeScript computes all valid combinations for us here and every invalid combination is a compile error.
Building the token system
Let's build this out for a real design system. Start with our primitive scales:
Now add semantic tokens on top. These are the named roles — primary, destructive, muted — that sit above the raw scale:
Now our component prop types write themselves:
Usage:
The satisfying is in the autocomplete in any editor with TypeScript language server support. The moment we type color="text- into a prop, your IDE offers every valid combination. We get both type safety and the ability to ship faster.
Try the interactive token builder:
type Color = 'slate' | 'blue' | 'emerald' | 'rose' | 'amber' | 'violet' | 'orange' | 'cyan' | 'fuchsia'
type Shade = '50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'
type ColorToken = `${Color}-${Shade}`
// resolves to 90 valid combinationsTailwind's static analysis
A potential challenge: Tailwind's JIT (Just-in-Time) compiler, the default and only engine in v3+, scans our source files for class strings at build time. But it does this with a simple regex; it can't evaluate JavaScript expressions. It only keeps the CSS classes that are hard-coded in our app. Dynamic class construction breaks it:
We have two good options around this.
Option 1: a lookup map (recommended)
It's a little bit verbose but gives us type safety.
Option 2: Tailwind safelist
Add our token patterns to tailwind.config.js:
This works but safelists can bloat our CSS bundle if we're not careful with the pattern scope. The lookup map approach keeps bundle size tight and makes the token surface explicit.