Template Literal Types

advanced template-literals string-types advanced-types

In simple language, template literal types are the type-level version of JS template strings. Same backtick + ${} syntax, except instead of building runtime strings we build string literal types. This unlocks crazy stuff like deriving event handler names from event names, validating URL patterns, and so on.

The basics

We can interpolate other types into a string template, and TS produces the union of every possible resulting string.

type Greeting = `Hello, ${string}`;
// matches any string starting with "Hello, "

const ok: Greeting = "Hello, Manish"; // works
// const bad: Greeting = "Hi there";  // error

If we plug a union in, TS distributes across all combinations. That’s where it gets interesting.

type Lang = "en" | "fr" | "de";
type Kind = "title" | "body";

type Key = `${Lang}_${Kind}`;
// "en_title" | "en_body" | "fr_title" | "fr_body" | "de_title" | "de_body"

Six exact strings, generated from two unions. No copy-paste, no mistakes.

Built-in string manipulation utilities

TS ships four intrinsic types for case conversion. They only work on string literal types, not generic string.

type A = Uppercase<"hello">;    // "HELLO"
type B = Lowercase<"WORLD">;    // "world"
type C = Capitalize<"manish">;  // "Manish"
type D = Uncapitalize<"Foo">;   // "foo"

Combine with mapped types and we get key remapping that actually does useful things. Here’s how to build typed event handler props from an event name union.

type Event = "click" | "hover" | "focus";

type EventHandlers = {
  [E in Event as `on${Capitalize<E>}`]: () => void;
};
// { onClick: () => void; onHover: () => void; onFocus: () => void }
"click" | "hover" + on${Capitalize} →
"click" → "onClick"
"hover" → "onHover"

Pattern matching with infer

Combine template literals with conditional types + infer and we can parse strings at the type level. Here’s a type that extracts the route param out of a path string.

type ExtractParam<T> = T extends `/${string}/:${infer Param}` ? Param : never;

type P1 = ExtractParam<"/users/:id">;     // "id"
type P2 = ExtractParam<"/posts/:slug">;   // "slug"
type P3 = ExtractParam<"/about">;         // never

The infer Param slot captures whatever sits after : in the matching path. We just wrote a tiny parser using only types. This is exactly how Express-style route typing works in libraries like hono or tRPC.

Real-world: type-safe API paths

Let’s say our API has paths like /users/:id/posts/:postId. We want a function that requires us to pass exactly those params.

type PathParams<P extends string> =
  P extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof PathParams<`/${Rest}`>]: string }
    : P extends `${string}:${infer Param}`
    ? { [K in Param]: string }
    : {};

function get<P extends string>(path: P, params: PathParams<P>) {
  // ... build URL
}

get("/users/:id", { id: "42" });                    // works
get("/users/:id/posts/:postId", { id: "1", postId: "2" }); // works
// get("/users/:id", { wrong: "x" });               // error

Yeah, that’s recursive template literal parsing. Heavy stuff, but it’s the same pattern used in real libraries we depend on.

Type-safe CSS units

A common practical use — restrict a string to a value with a known suffix.

type Pixels = `${number}px`;
type Percent = `${number}%`;
type CSSLength = Pixels | Percent | "auto";

const w1: CSSLength = "200px";   // works
const w2: CSSLength = "50%";     // works
const w3: CSSLength = "auto";    // works
// const w4: CSSLength = "200";  // error — missing unit

${number} is a special form that matches any numeric literal in a template string. There’s also ${bigint} and ${boolean}.

When to reach for it

Template literal types are addictive — we can model wildly complex string formats with them. But they’re also expensive to evaluate and hard to debug. Reach for them when modeling string formats that already exist in our system (event names, route paths, CSS values). For free-form strings, plain string is still the right call.