Template Literal Types

Reviewed & published by Brayan K

Template literal types build precise string types from a backtick pattern with interpolated types, letting you describe whole families of valid strings — route paths, event names, CSS classes — entirely at compile time.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Think of a fill-in-the-blanks form letter: "Dear ___, your ___ is ready." The fixed words never change; the blanks accept specific values. A template literal type is that form letter at the type level — the literal parts are fixed, and each placeholder restricts what can go in the blank. Hand it a union of options and it prints every valid letter at once.

1. String Types From Patterns

A template literal type uses backticks and ${...} placeholders, just like a template string — but it produces a type, not a value. Drop in a literal (`hello ${World}`) and you get an exact string; drop in the string placeholder (`/${string}`) and you get "any string matching this shape." At runtime you generate the actual values with ordinary template strings.

// A template literal TYPE interpolates other types into a string pattern:
type World    = "world";
type Greeting = `hello ${World}`;        // "hello world"

// Use 'string' as a placeholder for "any string here":
type Path = `/${string}`;                 // any string starting with "/"
const ok: Path = "/users";                // ✅
// const bad: Path = "users";             // ❌ must start with "/"

// Event-handler names from a property name:
type Handler = `on${Capitalize<"click">}`; // "onClick"
// TEMPLATE LITERAL TYPES build string TYPES the way template strings build
// values. In TS:  type Greeting = `Hello, ${string}`;
//   meaning "any string that starts with 'Hello, '".
// At runtime we use ordinary template strings to PRODUCE those values.

function greet(name) {
  return `Hello, ${name}!`;     // value side of the same idea
}
console.log(greet("Ada"));      // Hello, Ada!

// A type like `on${Capitalize<string>}` describes event-handler names.
// We can BUILD such a name at runtime:
function handlerName(event) {
  return "on" + event[0].toUpperCase() + event.slice(1);
}
console.log(handlerName("click")); // onClick
console.log(handlerName("focus")); // onFocus

// In a .ts file the TYPE would be:
//   type Handler = `on${Capitalize<"click" | "focus">}`; // "onClick" | "onFocus"

2. Unions Distribute Into a Cross Product

The real power appears with unions. When you interpolate a union into a template literal type, TypeScript generates every combination — the cross product. Two unions of two members each produce four string literals. This lets one short type stand in for a large, exact set of valid strings, like every size-color CSS class your design system allows.

// Unions inside a template literal type produce the CROSS PRODUCT:
type Size  = "sm" | "lg";
type Color = "red" | "blue";
type Class = `${Size}-${Color}`;
// "sm-red" | "sm-blue" | "lg-red" | "lg-blue"

const c: Class = "lg-blue";   // ✅ one of the four
// const x: Class = "md-red"; // ❌ not in the generated union

// Two unions of N and M members generate N*M string literals — a compact way
// to describe a whole family of valid strings without listing them by hand.
// When a template literal type uses a UNION, it DISTRIBUTES — producing every
// combination. In TS:
//   type Size  = "sm" | "lg";
//   type Color = "red" | "blue";
//   type Class = `${Size}-${Color}`;
//     -> "sm-red" | "sm-blue" | "lg-red" | "lg-blue"   (the cross product!)
// At runtime we generate the same set of strings with loops:

const sizes  = ["sm", "lg"];
const colors = ["red", "blue"];
const classes = [];
for (const s of sizes) {
  for (const c of colors) {
    classes.push(s + "-" + c);
  }
}
console.log(classes.join(", ")); // sm-red, sm-blue, lg-red, lg-blue
console.log(classes.length);     // 4

// This is why a single template literal type can stand in for dozens of
// hand-written string literals.

3. Intrinsic Transformers: Uppercase & Friends

TypeScript ships four built-in string-literal transformers: Uppercase<T>, Lowercase<T>, Capitalize<T>, and Uncapitalize<T>. They operate on string literal types at compile time, and combined with template literals they let you derive related names — turning a key like name into the method name getName with zero runtime cost.

// Four built-in (intrinsic) string-literal transformers:
type A = Uppercase<"hello">;     // "HELLO"
type B = Lowercase<"HELLO">;     // "hello"
type C = Capitalize<"hello">;    // "Hello"
type D = Uncapitalize<"Hello">;  // "hello"

// Combine with template literals to derive related names from a key set:
type Keys    = "name" | "age";
type Getters = `get${Capitalize<Keys>}`; // "getName" | "getAge"

// This powers helpers that, e.g., turn a model type into a getters interface
// entirely at the type level — no runtime cost.
// TS ships INTRINSIC string types: Uppercase, Lowercase, Capitalize,
// Uncapitalize. They transform string literal TYPES at the type level.
// In TS:  type Loud = Uppercase<"hello">;     // "HELLO"
//         type Cap  = Capitalize<"hello">;    // "Hello"
// At runtime the equivalent is ordinary string methods:

console.log("hello".toUpperCase());                        // HELLO
console.log("HELLO".toLowerCase());                        // hello
console.log("hello"[0].toUpperCase() + "hello".slice(1));  // Hello (Capitalize)

// A common real pattern: turn a property name into a getter name.
// In TS the TYPE would be:  `get${Capitalize<"name">}`  ->  "getName"
function getterName(prop) {
  return "get" + prop[0].toUpperCase() + prop.slice(1);
}
console.log(getterName("name"));  // getName
console.log(getterName("age"));   // getAge

// Template literal types + intrinsics = type-safe string transformations.

🎯 Your Turn

Event names often follow a `${field}:changed` pattern. Fill in the blank marked ___ with the fixed suffix, then run it.

// 🎯 YOUR TURN — replace the ___ then press "Try it Yourself".
// In TS:  type EventName = `${string}:changed`;
// Build event names like "price:changed" by appending a fixed suffix.

function eventName(field) {
  return `${field}:___`;   // 👉 the literal suffix: changed
}

console.log(eventName("price")); // price:changed
console.log(eventName("name"));  // name:changed

// ✅ Expected output:
//    price:changed
//    name:changed

Common Errors (and the fix)

Pro Tips

📚 Keep learning

Mini-Challenge: Generate Getters

No blanks this time — just a brief and a starting outline. Build the getter factory yourself, run it, and check your output against the example in the comments.

// 🎯 MINI-CHALLENGE: build getters from an object's keys.
// In TS the result TYPE would be  { [K in keyof T as `get${Capitalize<K>}`]: () => T[K] }.
// Here we build the RUNTIME equivalent.
//
// Write makeGetters(obj) that returns a NEW object where each key 'name'
// becomes a method 'getName' that returns obj.name:
//   1. For each key, compute "get" + Capitalized key.
//   2. Store a function that returns the original value.
//
// ✅ Expected output:
//    Ada
//    36
//    getName, getAge

// your code here
// const api = makeGetters({ name: "Ada", age: 36 });

🎉 Lesson Complete

Practice quiz

What does a template literal type build?

  • A runtime string
  • A number type
  • A string type from a backtick pattern with interpolated types
  • A class

Answer: A string type from a backtick pattern with interpolated types. It uses backtick-and-placeholder syntax at the type level to describe string shapes.

What does the type `/${string}` accept?

  • Any string that starts with a slash
  • Any string
  • Only the empty string
  • Only the literal '/string'

Answer: Any string that starts with a slash. The 'string' placeholder means any string here, but the leading / is fixed.

What happens when you interpolate a union into a template literal type?

  • It errors
  • It picks the first member
  • It widens to string
  • It distributes, producing the cross product of all combinations

Answer: It distributes, producing the cross product of all combinations. `${'sm'|'lg'}-${'red'|'blue'}` yields all four: sm-red, sm-blue, lg-red, lg-blue.

How many members does combining two 2-member unions produce?

  • 2
  • 4
  • 3
  • 8

Answer: 4. Two unions of N and M members generate N*M string literals - here 2*2 = 4.

What is Capitalize<'hello'>?

  • 'Hello'
  • 'HELLO'
  • 'hello'
  • 'hELLO'

Answer: 'Hello'. Capitalize uppercases the first character: 'Hello'.

Which are the four intrinsic string-manipulation types?

  • Trim, Pad, Split, Join
  • Upper, Lower, First, Last
  • Uppercase, Lowercase, Capitalize, Uncapitalize
  • ToString, ToNumber, ToBool, ToArray

Answer: Uppercase, Lowercase, Capitalize, Uncapitalize. TypeScript ships Uppercase, Lowercase, Capitalize, and Uncapitalize as intrinsics.

Do intrinsic transformers like Capitalize have a runtime form?

  • Yes, they run as functions
  • No - they are type-only; use string methods at runtime
  • Only Uppercase does
  • Yes, in strict mode

Answer: No - they are type-only; use string methods at runtime. They operate on string literal types at compile time; the runtime equivalent is toUpperCase() etc.

What does `on${Capitalize<'click'>}` evaluate to?

  • 'onclick'
  • 'on click'
  • 'Click'
  • 'onClick'

Answer: 'onClick'. Capitalize<'click'> is 'Click', so the result is 'onClick'.

What is a risk of very large unions in template literal types?

  • Runtime crashes
  • The cross product multiplies fast and can slow type-checking
  • They become any
  • They lose precision

Answer: The cross product multiplies fast and can slow type-checking. Two 50-member unions make 2,500 literals; huge unions hurt compile times.

Which is a good practical use of template literal types?

  • Math operations
  • Replacing functions
  • Typing route paths and deriving names like getName from keys
  • Runtime validation

Answer: Typing route paths and deriving names like getName from keys. They encode string conventions (route paths, event/handler names) in the type system.

Continue this course

Frequently asked questions

What is a template literal type?

It's a type that describes a string built from a pattern, using the same backtick-and-placeholder syntax as a template string — but at the type level. For example `Hello, ${string}` is the type of every string that starts with 'Hello, '. You can interpolate other types (string literals, unions, the 'string' placeholder) to express precise string shapes like route paths, CSS class names, or event names.

Why does a union inside a template literal type explode into many types?

Template literal types distribute over unions, producing the cross product of every combination. If you write `${'sm' | 'lg'}-${'red' | 'blue'}`, TypeScript generates all four results: 'sm-red', 'sm-blue', 'lg-red', 'lg-blue'. This lets one concise type stand in for a whole family of valid strings, which is far more maintainable than listing them by hand — but be aware that very large unions can multiply into thousands of members and slow the compiler.

What are Uppercase, Lowercase, Capitalize, and Uncapitalize?

They're intrinsic (compiler-built-in) string-literal transformer types. Uppercase<'hello'> is 'HELLO', Capitalize<'hello'> is 'Hello', and so on. They operate purely on string literal types at compile time and have no runtime form — the runtime equivalents are ordinary methods like toUpperCase(). They're most useful combined with template literals to derive related names, such as turning a key 'name' into 'getName'.

What are template literal types good for in practice?

Common uses include typing route paths (`/api/${string}`), CSS class or design-token names, event names like 'click' becoming 'onClick', and deriving getter/setter method names from a model's keys via mapped types. They let you encode string conventions in the type system so typos and invalid combinations are caught at compile time rather than discovered at runtime.