Template Literal Types: Mapping APIs with Precision

Felipe Hlibco

TypeScript 4.1 dropped on November 17th and I’ve spent the last two weeks playing with template literal types. They’re one of those features that sounds academic until you see what they unlock for real API modeling.

The short version: you can now use backtick syntax in type positions to construct and manipulate string literal types at compile time.

type EventName = `on${Capitalize<string>}`;
// Matches "onClick", "onHover", "onSubmit", etc.

That’s not a runtime string template—it’s a type-level computation. The compiler evaluates it during type checking and narrows accordingly.

Why this matters for API design #

I’ve been working with TypeScript for years at this point, and one recurring pain has been modeling string-based APIs with precision. Route parameters, event names, CSS properties—these all follow patterns that the type system couldn’t express before 4.1.

Take route parameters. A framework like Express defines routes as strings: /users/:id/posts/:postId. Before template literal types, you’d type the params object as Record<string, string> or write manual type definitions for each route. Both options are bad. One loses type safety; the other doesn’t scale.

Now you can extract params from the route pattern itself:

type ExtractParams<T extends string> =
  T extends `${infer _}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : T extends `${infer _}:${infer Param}`
      ? Param
      : never;

type Params = ExtractParams<"/users/:id/posts/:postId">;
// type Params = "id" | "postId"

The compiler infers "id" | "postId" from the route string. No codegen, no manual definitions. The route pattern is the single source of truth.

Event emitter patterns #

Event emitters are another natural fit. Most event systems follow a naming convention: click maps to onClick, change maps to onChange. TypeScript 4.1 makes this convention enforceable at the type level.

type EventHandler<T extends string> = `on${Capitalize<T>}`;

type ClickHandler = EventHandler<"click">;
// type ClickHandler = "onClick"

The Capitalize here is one of four new intrinsic string manipulation types. The full set:

  • Uppercase<T>—converts to all caps
  • Lowercase<T>—converts to all lowercase
  • Capitalize<T>—uppercases the first character
  • Uncapitalize<T>—lowercases the first character

These are compiler intrinsics, not library types. They operate on string literal types directly.

Where this gets powerful is combining template literals with mapped types. You can generate an entire event handler interface from a union of event names:

type Events = "click" | "focus" | "blur";

type EventHandlers = {
  [E in Events as `on${Capitalize<E>}`]: (event: E) => void;
};
// {
//   onClick: (event: "click") => void;
//   onFocus: (event: "focus") => void;
//   onBlur: (event: "blur") => void;
// }

The as clause in the mapped type (also new in 4.1) lets you remap keys using template literals. That’s two features working together to produce something neither could do alone.

CSS property transformations #

Here’s a practical one. CSS custom properties follow the --property-name convention, while JavaScript access uses camelCase via the style API. You can model that transformation:

type CamelCase<S extends string> =
  S extends `${infer Head}-${infer Tail}`
    ? `${Head}${Capitalize<CamelCase<Tail>>}`
    : S;

type Result = CamelCase<"background-color">;
// type Result = "backgroundColor"

The recursion handles multi-segment properties like border-top-width by processing each - boundary. It’s recursive type computation on strings, and the compiler handles it without breaking a sweat.

The bigger picture on type-level programming #

I’ve written before about TypeScript 4.1’s broader feature set (the beta post from a few months back covered key remapping and recursive conditional types). Template literal types are the piece that ties it all together. Recursive conditional types give you control flow at the type level. Key remapping gives you object transformation. Template literals give you string manipulation.

Combined, these three features push TypeScript’s type system toward something that resembles a functional programming language operating entirely at compile time. You can parse strings, transform data structures, and enforce conventions without writing a single line of runtime code.

Is this useful for everyday application code? Honestly, probably not directly. Most of us won’t write recursive type-level string parsers in our feature branches. But library authors will. Framework authors will. And when they do, the error messages and autocomplete suggestions that flow downstream to application developers get dramatically better.

That’s the real win. Not that you can write ExtractParams<"/users/:id">, but that your router library does it for you and catches typos at compile time.

One caveat #

Template literal types with recursive inference can hit the compiler’s recursion depth limits on complex inputs. TypeScript has a hardcoded depth limit (currently around 50 levels) for recursive type evaluation. For most practical patterns this is fine, but if you’re trying to parse arbitrary-length strings at the type level, you’ll hit walls.

The TypeScript team has been clear that these limits exist to prevent the compiler from hanging on pathological types. Fair enough. But it means template literal types work best for well-structured, bounded string patterns—route params, event names, config keys—rather than arbitrary text processing.

Still, for API modeling? This is a genuine step forward. I’m looking forward to seeing how library authors adopt it.