ESC

Type to search the knowledge base.

Template Literal Types

Build string types with `on${Event}`, extract route params, and type CSS-like APIs using TypeScript template literal types.

advanced3 min read
  • typescript
  • template-literal

Template literal types mirror JS template strings in the type system:

type World = 'world';
type Greeting = `hello ${World}`; // 'hello world'

They compose unions:

type Corner = `${'top' | 'bottom'}-${'left' | 'right'}`;
// 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'

Docs: Template Literal Types.

Event handler prop names

type Ev = 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<Ev>}`;
// 'onClick' | 'onFocus' | 'onBlur'

Intrinsic string modifiers: Uppercase, Lowercase, Capitalize, Uncapitalize.

Extracting pieces with infer

type ParseEvent<T> = T extends `on${infer E}` ? Uncapitalize<E> : never;

type E = ParseEvent<'onClick'>; // 'click'
type RouteParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | RouteParams<`/${Rest}`>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

type P = RouteParams<'/users/:id/posts/:postId'>; // 'id' | 'postId'

(Real routers use battle-tested types; this shows the pattern.)

CSS unit patterns

type CssLength = `${number}px` | `${number}rem` | `${number}%` | 0;

function margin(value: CssLength) {
  return { margin: value };
}

margin('8px');
// margin('8em'); // error if not in union

API path builders

type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'orders';
type Endpoint = `/api/${ApiVersion}/${Resource}`;

function get(path: Endpoint) {
  return fetch(path);
}

get('/api/v1/users');

Mapped keys with templates

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

See mapped types intro.

Pattern matching unions

type Email = `${string}@${string}.${string}`; // weak but documents intent

Don’t over-trust string structure types — still validate emails at runtime.

When they shine in frontend

  1. Design-token names: color.primary.500 style paths.
  2. i18n keys if you generate a key union.
  3. Query param builders.
  4. Library typings for CSS-in-JS.

Complexity costs

Template literal types can explode unions (A × B × C). IDE performance suffers. Keep unions small; split intermediate aliases; avoid open string interpolations when a finite union works.

// Can get huge
type Huge = `${'a' | 'b' | 'c'}-${'1' | '2' | '3'}-${'x' | 'y' | 'z'}`;

Footguns

  1. ${string} absorbs almost everything — constraints go soft.
  2. Number interpolation — `$&#123;number&#125;` is a pattern, not math.
  3. Runtime — types don’t format strings; write real functions.
  4. Recursion depth — deep route parsers need care.

Interview out-loud answer

“Template literal types compose string unions and can infer chunks with infer. I use them for event prop names, path patterns, and mapped getter keys. I keep unions finite for performance and still validate user strings at runtime.”

Uppercase helpers in real APIs

type Http = 'get' | 'post';
type Method = Uppercase<Http>; // 'GET' | 'POST'

Intrinsic string modifiers are compile-time only. Runtime still needs .toUpperCase() if you construct strings dynamically — types won’t convert values.

Extra practice

Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.

Further reading

Related guides