We’ve conquered custom type guards, automated state with discriminated unions, and performed compile-time transformations with conditional and mapped types.
Now, let's explore one of TypeScript's most creative and powerful features: Template Literal Types.
Template literal types allow us to construct new string types using string literal types, mirroring how template literals work in JavaScript runtime code. When combined with recursive conditional types, they let us build completely type-safe APIs for things that used to be pure string manipulation—like routing and URL parameters.
1. What is a Template Literal Type?
Just like JavaScript template strings use backticks (`Hello ${name}`), TypeScript allows us to interpolate string literal types at the type level.
type World = "world"; type Greeting = `Hello ${World}`; // Type is "Hello world" We can combine unions to generate combinatorial string types automatically:
type Side = "top" | "bottom"; type Axis = "x" | "y"; // Automatically evaluates to: "margin-top-x" | "margin-top-y" | "margin-bottom-x" | "margin-bottom-y" type MarginStyle = `margin-${Side}-${Axis}`; TypeScript's Built-in String Helpers
TypeScript also provides four intrinsic helper types for transforming string literal types:
Uppercase<T>Lowercase<T>Capitalize<T>Uncapitalize<T>
2. Real-World Power: Building Type-Safe Routing
In traditional web frameworks, defining routes is prone to string typos. We might write navigate("/users/123/posts/45"), but if we misspell a parameter or forget one, we only find out at runtime.
With template literal types, we can build a parser that reads a route string at compile time, extracts its parameters, and forces us to pass the correct payload.
Step 1: Extracting Route Parameters
We can use template literal types combined with infer to match segments starting with a colon (:):
// Recursively extracts parameter names from a route string type ExtractRouteParams<T extends string> = T extends `${infer _Start}/:${infer Param}/${infer Rest}` ? Param | ExtractRouteParams<`/${Rest}`> : T extends `${infer _Start}/:${infer Param}` ? Param : never; Let's test this mental compiler:
type ProfileRoute = "/users/:userId/settings/:tab"; type Params = ExtractRouteParams<ProfileRoute>; // Evaluates to: "userId" | "tab" 🎉 Step 2: Creating a Type-Safe Router Function
Now, we use our extraction utility to type a navigation function. The compiler will demand that we pass an object containing exact keys matching whatever parameters exist in our route string.
// Define a helper type for the params object type RouteParams<T extends string> = Record<ExtractRouteParams<T>, string>; // Type-safe navigate function function navigate<T extends string>(route: T, params: RouteParams<T>) { let finalUrl: string = route; // Replace placeholders with actual values at runtime for (const [key, value] of Object.entries(params)) { finalUrl = finalUrl.replace(`:${key}`, value); } console.log(`Navigating to: ${finalUrl}`); // window.history.pushState({}, "", finalUrl); } Step 3: Seeing it in Action
Watch how strict and helpful TypeScript becomes:
// ✅ Valid: Matches ":userId" and ":postId" navigate("/users/:userId/posts/:postId", { userId: "user_99", postId: "post_404" }); // ❌ Compile Error: Property 'postId' is missing in type '{ userId: string; }' // but required in type 'Record<"userId" | "postId", string>' navigate("/users/:userId/posts/:postId", { userId: "user_99" }); // ❌ Compile Error: Object literal may only specify known properties, // and 'extraKey' does not exist in type 'Record<"userId" | "postId", string>' navigate("/users/:userId/posts/:postId", { userId: "user_99", postId: "post_404", extraKey: "oops" }); Series Conclusion
Over the course of this series, we've journeyed from basic runtime safety to advanced type-level programming:
- Custom Type Guards (
value is Type) to tame untrusted inputs. - Discriminated Unions for bulletproof internal state machines.
- Advanced Generics, Mapped & Conditional Types for deep type transformations.
- Template Literal Types to parse and enforce structures inside strings.
TypeScript isn't just a linter checking our code—it's a rich type-level language capable of modeling complex business logic long before our app ever runs in a browser.