RizTech Academy logo
RizTech Academy
The Type System in DepthLesson 5 of 630 min

Template literal types

Template literal types bring string-template syntax to the type level: just as `Hello, ${name}` builds a string from parts at runtime, `Hello, ${T}` builds a type from type parts at compile time. They let you describe and construct string types with structure — an event name, a CSS property, a route path, an API endpoint — with the compiler checking the string's shape, not just that it is "a string". This is TypeScript doing something almost no other language's type system can.

The syntax

A template literal type uses backticks and ${}, with types in the interpolation:

type Greeting = `Hello, ${string}`;

const a: Greeting = "Hello, Kavita";    // fine — matches the pattern
const b: Greeting = "Hi, Kavita";       // error: not assignable to `Hello, ${string}`
const c: Greeting = "Hello, ";          // fine — ${string} can be empty

`Hello, ${string}` is the type of any string that starts with "Hello, ". TypeScript checks the string against the pattern — "Hi, Kavita" does not start with "Hello, ", so it is rejected. You have gone from "any string" to "a string with this specific shape", checked at compile time.

Building types from literal unions

The real power comes from interpolating literal unions — TypeScript expands the template over every combination:

type Color = "red" | "green" | "blue";
type Shade = "light" | "dark";

type ColorShade = `${Shade}-${Color}`;
// "light-red" | "light-green" | "light-blue" | "dark-red" | "dark-green" | "dark-blue"

`${Shade}-${Color}` produces every combination of a shade and a colour — six literal types, generated automatically. Add a colour to Color and all its combinations appear. This is how you type things like CSS class names, BEM modifiers, or any structured string built from known parts, with the compiler ensuring only valid combinations are used:

type Route = `/${"users" | "orders" | "products"}/${string}`;

const r1: Route = "/users/123";      // fine
const r2: Route = "/comments/5";     // error: "comments" is not one of the allowed segments

Route describes a URL path whose first segment must be one of three known values — a typo like /comments/5 is a compile error. This is genuinely useful for typed routing and APIs.

The intrinsic string manipulators

TypeScript ships four built-in types that transform string literals at the type level — Uppercase, Lowercase, Capitalize, Uncapitalize:

type A = Uppercase<"pune">;        // "PUNE"
type B = Lowercase<"PUNE">;        // "pune"
type C = Capitalize<"pune">;       // "Pune"
type D = Uncapitalize<"Pune">;     // "pune"

These operate on the types, not values, and they combine with template literals to transform strings structurally. The classic use is generating method names from property names — the Getters example from the mapped-types lesson, now fully explained:

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

interface User { name: string; age: number; }

type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }

`get${Capitalize<string & K>}` takes each key (name), capitalises it (Name), and prefixes get (getName) — all in the type. This is how a library generates a getter/setter interface, or event handler names (onClick, onHover) from an events type, entirely in the type system.

Extracting parts with infer

Template literal types compose with infer (last lesson) to parse strings at the type level — pulling structured pieces out of a string literal type:

type ExtractId<T> = T extends `user_${infer Id}` ? Id : never;

type A = ExtractId<"user_123">;    // "123"  — captured the part after "user_"
type B = ExtractId<"order_5">;     // never   — does not match the pattern

T extends `user_${infer Id}` matches a string of the form user_something and captures the something. This is type-level string parsing — you can split, extract prefixes, and decompose string types. Libraries use it to derive typed parameters from route patterns (/users/:id → { id: string }), which is a genuinely magical-feeling feature when you first meet it.

When to use them — and the honest caution

Template literal types are powerful and, like conditional types, mostly something you read rather than write. Their legitimate everyday uses in application code:

  • Constraining a string's shape — an event name that must be on${Capitalize<EventName>}, a route, a prefixed key, a CSS value. When a string must follow a pattern, this checks it.
  • Deriving related string types — method names from properties, class names from parts.

The caution is the same as the whole module's: do not build elaborate type-level string machinery for its own sake. A codebase where simple things are typed with baroque template-literal-and-conditional gymnastics is harder to work in, not safer — the over-engineering the design-patterns module warns against, at the type level. Reach for a template literal type when a string genuinely has a shape you want checked, or when deriving related string types keeps things in sync. Otherwise a plain literal union ("onClick" | "onHover") is clearer than generating the same union with a template. Power is for when you need it; clarity is always.

Check your work

What a template literal type is. Type-level string templates — `Hello, ${string}` — describing a string's shape, checked at compile time.

What interpolating a literal union does. Expands over every combination — `${Shade}-${Color}` generates all shade-colour pairs.

A practical use. Constraining structured strings — routes, class names, prefixed keys — so a typo in the shape is a compile error.

The four intrinsic manipulators. Uppercase, Lowercase, Capitalize, Uncapitalize — transform string literals at the type level.

How method names are generated. Key remapping + `get${Capitalize<K>}` — the Getters pattern.

Composing with infer. T extends `user_${infer Id}` parses a string type, capturing a part — type-level string parsing (typed routes).

The everyday uses. Constraining a string's shape, and deriving related string types in sync.

The caution. Use them where a string genuinely has a shape worth checking; do not build elaborate type-level string machinery for its own sake — a plain literal union is often clearer.

Practice

  1. Write type Greeting = `Hello, ${string}` and assign a matching and non-matching string. Read the error.
  2. Build a `${Shade}-${Color}` type from two literal unions and confirm all combinations exist.
  3. Write a Route template type and assign a valid and an invalid path.
  4. Use Uppercase, Capitalize, and the others on string literals and confirm the results.
  5. Build a Getters<T> with key remapping and template literals; confirm name becomes getName.
  6. Write ExtractId<T> = T extends `user_${infer Id}` ? Id : never and extract the id from "user_123".
  7. Find a place you would use a template literal type, and one where a plain literal union is clearer. Justify each.

Official documentation

Next: building your own utility types, and when to stop.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship