Skip to Content
Course content

32: Template Literal Types

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're managing a small coffee shop. You have a specific list of sizes (Small, Medium, Large) and a specific list of roast types (Light, Medium, Dark). Instead of just letting people order "whatever," you've decided that every internal order ticket must follow a strict format: Size-Roast. So, a ticket might say Small-Light or Large-Dark.

In your head, you've just created a rule. You didn't just say "the ticket is a string"; you said "the ticket is a combination of one of these three sizes and one of these three roasts." Template Literal Types in TypeScript let us do exactly that with our type system. We aren't just defining a string; we're defining a pattern that strings must follow.

The Magic of String Interpolation for Types

You're already used to template literals in JavaScript for creating strings at runtime. TypeScript brings that same syntax to the type level. I find this incredibly useful when I'm dealing with things like event names or API endpoints where the naming convention is strict.

Let's look at a real-world scenario: a notification system. You might have different categories of notifications and different severity levels. Instead of typing out every single possible combination manually, we can let TypeScript do the math for us:

type Category = "user" | "system" | "billing";
type Severity = "info" | "warning" | "error";

// This is where the magic happens
type NotificationEvent = `${Category}_${Severity}`;

// Valid:
const event1: NotificationEvent = "user_info";
const event2: NotificationEvent = "billing_error";

// Error: "user_critical" (critical isn't in Severity)
// Error: "admin_info" (admin isn't in Category)
const event3: NotificationEvent = "user_critical"; 

Notice how TypeScript automatically expands NotificationEvent to be a union of all 9 possible combinations. If you hover over the type in your IDE, you'll see the full list. It's a huge time-saver and prevents those annoying typos that usually only surface as bugs in production.

Combining Types to Build a Matrix

The real power kicks in when you start nesting these or combining them with other literal types. I often use this when building UI components that have "variants."

Suppose you're building a set of CSS-like utility classes for a design system. You have directions (top, bottom, left, right) and spacing scales (1, 2, 4, 8). You can create a type that ensures you only use valid spacing classes:

type Direction = "top" | "bottom" | "left" | "right";
type Scale = 1 | 2 | 4 | 8;

type SpacingClass = `m-${Direction}-${Scale}`;

const myMargin: SpacingClass = "m-top-4"; // Perfectly valid
const badMargin: SpacingClass = "m-center-4"; // Error! "center" isn't a Direction

I love this because it turns the compiler into a design system enforcer. You don't have to memorize the documentation; the autocomplete literally tells you what's allowed.

Taming the Chaos with Constraints

A word of caution: be careful not to overdo it. If you combine five different unions with ten members each, you'll end up with a type that has 100,000 possible permutations. TypeScript can actually start to lag or throw a "type instantiation is excessively deep" error if the combinations explode too quickly.

If you find yourself hitting that wall, it's usually a sign that your type system is trying to do too much of the logic that should probably just be a simple string check at runtime. Use template literal types for patterns that are small enough to be manageable but strict enough to be useful.




📋 Practical Task

Exercise: Building a Type-Safe API Endpoint Generator

You are building a client for a REST API. The API follows a strict naming convention for its endpoints: it always starts with api/, followed by a resource name, and ends with a specific action.

Your task: Create a type-safe system to ensure endpoints are constructed correctly.

  • Define a type Resource that can be "users", "posts", or "comments".
  • Define a type Action that can be "get", "create", "update", or "delete".
  • Create a Template Literal Type called ApiEndpoint that forces the format: api/[Resource]/[Action].
  • Declare a variable currentRoute of type ApiEndpoint and assign it a valid path (e.g., "api/users/get").
  • Try to assign an invalid path (like "api/products/get" or "api/users/fetch") to see the TypeScript compiler catch the error.
// Start your code here:
type Resource = // ...
type Action = // ...
type ApiEndpoint = // ...

const currentRoute: ApiEndpoint = // ...
Rating
0 0

There are no comments for now.

to be the first to leave a comment.