Template Literal Types

Template literal types use the same backtick syntax as JavaScript template strings, but at the type level. They let you describe string formats — CSS units, event names, ID prefixes, URL paths — and have the compiler check them.

Building string types

TS basic.ts
type Size = "sm" | "md" | "lg";
type Color = "red" | "blue";

type ButtonClass = `btn-${Size}-${Color}`;

const ok: ButtonClass = "btn-md-blue";
const bad: ButtonClass = "btn-xl-blue";
tsc output
error TS2820: Type '"btn-xl-blue"' is not assignable to type '"btn-sm-red" | "btn-sm-blue" | "btn-md-red" | "btn-md-blue" | "btn-lg-red" | "btn-lg-blue"'. Did you mean '"btn-lg-blue"'?

When unions are placed inside a template literal type, TypeScript produces every combination — here, 3 sizes × 2 colours = 6 strings. The error message lists all six and even suggests the closest match.

Patterns with primitive placeholders

You can also use string, number or bigint inside the template to describe an open-ended pattern rather than a fixed list:

TS patterns.ts
type Pixels = `${number}px`;
type OrderId = `ORD-${number}`;
type Email = `${string}@${string}.${string}`;

const width: Pixels = "120px";
const id: OrderId = "ORD-1042";
const contact: Email = "hi@example.com";

const wrong: Pixels = "120em";
tsc output
error TS2322: Type '"120em"' is not assignable to type '`${number}px`'.

These are compile-time checks on string literals. A string that arrives at runtime (from a form or API) is just string, so you'd still validate it — often with a type guard that returns value is Email.

Combining with string-case utilities

The intrinsic types Uppercase, Lowercase, Capitalize and Uncapitalize transform literal types:

TS case.ts
type Method = "get" | "post" | "delete";
type HttpVerb = Uppercase<Method>;
type HandlerName = `handle${Capitalize<Method>}`;

const verb: HttpVerb = "POST";
const handler: HandlerName = "handleDelete";
console.log(verb, handler);
Output
POST handleDelete

Typed event names

A common real-world use is deriving event or handler names from data keys:

TS events.ts
type Fields = { name: string; age: number };
type ChangeEvent = `${keyof Fields & string}Changed`;

function on(event: ChangeEvent, cb: () => void) {
  console.log(`listening for ${event}`);
  cb();
}

on("ageChanged", () => console.log("age handler ran"));
Output
listening for ageChanged
age handler ran

ChangeEvent is "nameChanged" | "ageChanged". Add a field and you get a new valid event name for free; a typo like "agChanged" is a compile error.

Parsing strings with infer

In a conditional type, template literals can also match strings and capture parts of them:

TS parse.ts
type RouteParams<Path extends string> =
  Path extends `${string}:${infer Param}/${infer Rest}`
    ? Param | RouteParams<`/${Rest}`>
    : Path extends `${string}:${infer Param}`
      ? Param
      : never;

type Params = RouteParams<"/users/:userId/posts/:postId">;

function link(params: Record<Params, string>) {
  return `/users/${params.userId}/posts/${params.postId}`;
}

console.log(link({ userId: "7", postId: "42" }));
Output
/users/7/posts/42

Params works out to "userId" | "postId" from the route string alone. Routers such as React Router and Hono use this technique to type route parameters. It's an advanced trick — understanding how it reads is more important than writing one from scratch.

Template literal index signatures

As seen in lesson 25, template patterns can also be used as index signature keys:

TS css-vars.ts
type CSSVars = { [key: `--${string}`]: string };

const theme: CSSVars = {
  "--brand": "#d6336c",
  "--radius": "3px",
};

console.log(Object.keys(theme).join(", "));
Output
--brand, --radius

Watch the size

Combining several large unions in one template multiplies their sizes. Three unions of 20 members each produce 8,000 string types, which can slow your editor down noticeably. TypeScript refuses to build unions larger than 100,000 members.

Try it yourself

Try it yourself
Console output