Skip to content

TypeScript Template Literal Types — Complete Guide

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about TypeScript Template Literal Types. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript template literal types let you manipulate string types at compile time — concatenating, capitalizing, and transforming string literals to create precise, type-safe APIs for CSS properties, event names, and more.

What You'll Learn

  • Template literal type syntax
  • Intrinsic string manipulation types
  • Combining with unions for cartesian products
  • Real-world patterns: CSS types, event emitters, API routes

Why It Matters

Before template literal types (TS 4.1), string types were static. You could only say "this must be exactly 'hello'". Now you can say "this must start with 'on' followed by a capitalized word" — enabling type-safe event systems, CSS-in-JS, and API route builders without runtime validation.

Real-World Use

The Doda Browser extension API uses template literal types for its event system — "onTabCreated", "onTabRemoved", "onTabUpdated" are derived from a base event name type. This guarantees that addListener("onTabCreated") is valid while addListener("onTabCreatedd") is a compile error.

Learning Path

flowchart LR
  A[keyof typeof] --> B[Template Literal Types]
  B --> C[Conditional Types]
  B --> D[You Are Here]
  C --> E[Mapped Types]
  E --> F[Utility Types]

Basic Syntax

type Greeting = `Hello, ${string}!`;
// Any string that starts with "Hello, " and ends with "!"

const msg1: Greeting = "Hello, World!";   // OK
const msg2: Greeting = "Hello, Alice!";   // OK
// const msg3: Greeting = "Hi there!";    // Error

The ${} placeholder accepts any type that extends string | number | bigint | boolean | null | undefined.

String Literal Unions

The real power comes from using union types inside placeholders:

type Size = "small" | "medium" | "large";
type ButtonSize = `btn-${Size}`;
// "btn-small" | "btn-medium" | "btn-large"

const size1: ButtonSize = "btn-small";   // OK
const size2: ButtonSize = "btn-large";   // OK
// const size3: ButtonSize = "btn-xl";   // Error

Cartesian Product of Unions

When you have multiple placeholders with unions, TypeScript produces every combination:

type Color = "red" | "green" | "blue";
type Emphasis = "light" | "dark" | "neutral";

type ThemeColor = `${Emphasis}-${Color}`;
// "light-red" | "light-green" | "light-blue" |
// "dark-red"  | "dark-green"  | "dark-blue"  |
// "neutral-red" | "neutral-green" | "neutral-blue"

// 3 × 3 = 9 string literal types

Intrinsic String Types

TypeScript provides four built-in string manipulation types:

type Upper = Uppercase<"hello">;       // "HELLO"
type Lower = Lowercase<"HELLO">;       // "hello"
type Capital = Capitalize<"hello">;    // "Hello"
type Uncapital = Uncapitalize<"Hello">; // "hello"

These work with unions too:

type EventName = "click" | "submit" | "focus";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onSubmit" | "onFocus"

Pattern: Type-Safe Event Emitter

type BaseEvents = "click" | "submit" | "focus" | "blur";

// Generate handler names
type HandlerEvent = `on${Capitalize<BaseEvents>}`;
// "onClick" | "onSubmit" | "onFocus" | "onBlur"

// Generate event handler signatures
type EventHandlers = {
  [K in HandlerEvent]: (event: Event) => void;
};

// Use it
class TypedButton implements EventHandlers {
  onClick!: (event: Event) => void;
  onSubmit!: (event: Event) => void;
  onFocus!: (event: Event) => void;
  onBlur!: (event: Event) => void;
}

Pattern: CSS Properties

type CSSUnit = "px" | "em" | "rem" | "%" | "vh" | "vw";
type CSSValue = `${number}${CSSUnit}`;

const width: CSSValue = "100px";  // OK
const height: CSSValue = "50%";   // OK
// const invalid: CSSValue = "100"; // Error — missing unit

// Partial CSS property type
type CSSProperty = `margin${"" | "Top" | "Right" | "Bottom" | "Left"}`;
// "margin" | "marginTop" | "marginRight" | "marginBottom" | "marginLeft"

type CSSWithValue = {
  [K in CSSProperty]?: CSSValue;
};

const style: CSSWithValue = {
  margin: "10px",
  marginTop: "20px",
  marginLeft: "auto",
};

Pattern: API Route Builder

type Resource = "users" | "scans" | "threats" | "reports";
type Action = "list" | "get" | "create" | "update" | "delete";

type APIRoute = `/${Resource}`;
// "/users" | "/scans" | "/threats" | "/reports"

type APIRouteWithId = `/${Resource}/${number}`;
// "/users/123" | "/scans/123" | ...

type APIMethod = {
  [R in Resource]: {
    [A in Action]: string;
  };
};

type MethodMap = {
  [R in Resource as `/${R}`]: {
    [A in Action]: `/${R}/${A}`;
  };
};

Pattern: Formatted Logging

type LogLevel = "info" | "warn" | "error";
type LogMessage = `[${Uppercase<LogLevel>}] ${string}`;

const msg1: LogMessage = "[INFO] Server started";    // OK
const msg2: LogMessage = "[WARN] Disk space low";     // OK
const msg3: LogMessage = "[ERROR] Connection failed"; // OK
// const msg4: LogMessage = "Server started";         // Error

Advanced: Inferring from Template Literals

Combine with infer to extract parts of strings:

type ExtractId<T extends string> =
  T extends `/api/${infer Resource}/${infer Id}`
    ? { resource: Resource; id: Id }
    : never;

type Result1 = ExtractId<"/api/users/123">;
// { resource: "users"; id: "123" }

type Result2 = ExtractId<"/api/scans/scan-001">;
// { resource: "scans"; id: "scan-001" }

Common Mistakes

1. Forgetting That Template Literal Types Only Work with String/Number Literals

type Test<T extends string> = `hello-${T}`;
type Result = Test<string>; // string — too wide, no specific literals

Template literals need literal types (unions of literals) to produce useful results.

2. Creating Too Many Combinations

type A = "a" | "b" | "c";
type B = "d" | "e" | "f";
type Three = `${A}-${B}`; // 9 combinations — manageable

type Many = `${A}-${B}-${A}-${B}`; // 81 combinations — getting large

Very large template literal unions can slow down Type Checking.

3. Confusing Capitalize with Uppercase

Capitalize<"hello">    // "Hello" — first letter only
Uppercase<"hello">     // "HELLO" — all letters

4. Using String Concatenation Instead of Template Literals

// Can't use + for types
// type Bad = "on" + "Click"; // Error

// Use template literals
type Good = `on${"Click"}`; // "onClick"

5. Not Handling Empty Strings Properly

type Name = "" | "Alice" | "Bob";
type Greeting = `Hello ${Name}`;
// "Hello " | "Hello Alice" | "Hello Bob"
// Note: "Hello " (with trailing space) is valid!

Practice Questions

  1. What does ${string} mean inside a template literal type? A placeholder that matches any string, making the template match any string with the given prefix and suffix.

  2. How do you create a type that matches all possible event handlers for a set of events? Use type Handler = \on${Capitalize}`` where Event is a union of event name literals.

  3. What is the difference between Capitalize and Uppercase? Capitalize uppercases only the first character; Uppercase uppercases all characters.

  4. Can template literal types be used with numbers? Yes: type Size = `${number}px`; matches strings like "10px", "42px".

Challenge: Create a CSSClass type that represents BEM-style class names: block__element--modifier. Use template literals to validate that a string matches this pattern.

FAQ

Do template literal types have any runtime cost?

No. They are compile-time only. The output JavaScript is not affected.

Can I nest template literal types?

Yes: type Hello<T extends string> = `Hello, ${T}!`; type Greet = Hello<`Mr. ${string}`>;

What happens when I use a wide type like string in a placeholder?

The result is just string — no specific literal types can be inferred.

Can template literal types be recursive?

No, TypeScript does not support recursive template literal types directly.

Are template literal types available in all TypeScript versions?

They were introduced in TypeScript 4.1. For older versions, you need string literal unions manually.

Mini Project: Type-Safe CSS Style Builder

// src/css-builder.ts

type CSSUnit = "px" | "em" | "rem" | "%" | "vh" | "vw" | "pt";

type CSSValue = `${number}${CSSUnit}`;
type BorderStyle = "solid" | "dashed" | "dotted" | "none";

type ShorthandProperty = "margin" | "padding" | "border";
type Direction = "" | "Top" | "Right" | "Bottom" | "Left";

type CSSProperty = `${ShorthandProperty}${Direction}`;
type BorderProperty = `border${Direction}${"" | "Width" | "Style" | "Color"}`;

type StyleSheet = {
  [selector: string]: Partial<{
    [K in CSSProperty | BorderProperty]: string;
  }>;
};

function createStyle(styles: StyleSheet): string {
  let result = "";
  for (const [selector, props] of Object.entries(styles)) {
    result += `${selector} {\n`;
    for (const [prop, value] of Object.entries(props || {})) {
      result += `  ${prop}: ${value};\n`;
    }
    result += "}\n";
  }
  return result;
}

const styles = createStyle({
  ".btn": {
    margin: "10px",
    paddingTop: "5px",
    borderBottom: "2px solid red",
  },
  ".btn-primary": {
    padding: "8px 16px",
    borderStyle: "solid",
    marginLeft: "0px",
  },
});

console.log(styles);

Expected output:

.btn {
  margin: 10px;
  paddingTop: 5px;
  borderBottom: 2px solid red;
}
.btn-primary {
  padding: 8px 16px;
  borderStyle: solid;
  marginLeft: 0px;
}

What's Next

Now dive deep into conditional types:

Lesson Description
{{< ref "/programming-languages/typescript/11-keyof-typeof" >}} Review keyof and typeof
{{< ref "/programming-languages/typescript/13-conditional-types" >}} Conditional types with extends and infer
{{< ref "/programming-languages/typescript/14-mapped-types" >}} Mapped types and type transformations

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro