TypeScript Template Literal Types: String Manipulation
Template literal types are TypeScript's answer to type-level string manipulation. Introduced in TypeScript 4.1, they mirror JavaScript's template literal syntax but operate entirely at compile time....
Key Insights
- Template literal types bring compile-time string manipulation to TypeScript, enabling type-safe APIs that previously required runtime validation or code generation
- Built-in utilities like
Uppercase,Lowercase,Capitalize, andUncapitalizecombined with theinferkeyword unlock pattern matching and extraction at the type level - While powerful, deeply nested template literal types can cause compiler performance issues—keep recursion depth under 10 levels and use union distribution strategically
Introduction to Template Literal Types
Template literal types are TypeScript’s answer to type-level string manipulation. Introduced in TypeScript 4.1, they mirror JavaScript’s template literal syntax but operate entirely at compile time. This means you can transform, validate, and parse strings as types, catching errors before your code ever runs.
The basic syntax looks familiar:
type Greeting = `Hello ${string}`;
const valid: Greeting = "Hello World"; // ✓
const invalid: Greeting = "Hi there"; // ✗ Type error
This simple example demonstrates the core concept: you can create types that match specific string patterns. But the real power emerges when you combine template literals with generics, conditional types, and TypeScript’s built-in string manipulation utilities.
Built-in String Manipulation Utilities
TypeScript provides four intrinsic types for string transformation: Uppercase<T>, Lowercase<T>, Capitalize<T>, and Uncapitalize<T>. These work exactly as their names suggest, operating on string literal types.
Here’s a practical example transforming API endpoint names into environment variable keys:
type ApiEndpoint = "userProfile" | "orderHistory" | "paymentMethod";
type EnvKey<T extends string> = `API_${Uppercase<T>}_URL`;
type ApiEnvKeys = EnvKey<ApiEndpoint>;
// Result: "API_USERPROFILE_URL" | "API_ORDERHISTORY_URL" | "API_PAYMENTMETHOD_URL"
const config: Record<ApiEnvKeys, string> = {
API_USERPROFILE_URL: "https://api.example.com/user",
API_ORDERHISTORY_URL: "https://api.example.com/orders",
API_PAYMENTMETHOD_URL: "https://api.example.com/payment"
};
For CSS-in-JS libraries, you can enforce consistent naming conventions:
type CssPrefix = "btn" | "card" | "nav";
type CssVariant = "primary" | "secondary" | "danger";
type ClassName<P extends string, V extends string> =
`${P}-${Lowercase<V>}`;
type ButtonClass = ClassName<"btn", CssVariant>;
// Result: "btn-primary" | "btn-secondary" | "btn-danger"
function applyClass(className: ButtonClass) {
// TypeScript ensures only valid class names are passed
}
applyClass("btn-primary"); // ✓
applyClass("btn-invalid"); // ✗ Type error
Extracting and Parsing String Patterns
The infer keyword combined with template literals enables pattern matching and extraction. This is where template literal types become genuinely powerful.
Parsing route parameters from URL patterns:
type ParseRoute<T extends string> =
T extends `${infer Start}/:${infer Param}/${infer Rest}`
? Param | ParseRoute<`/${Rest}`>
: T extends `${infer Start}/:${infer Param}`
? Param
: never;
type UserRoute = "/users/:userId/posts/:postId";
type Params = ParseRoute<UserRoute>;
// Result: "userId" | "postId"
type RouteParams<T extends string> = {
[K in ParseRoute<T>]: string;
};
const params: RouteParams<UserRoute> = {
userId: "123",
postId: "456"
};
Extracting file extensions:
type GetExtension<T extends string> =
T extends `${infer Name}.${infer Ext}`
? Ext
: never;
type ImageExt = GetExtension<"photo.jpg">; // "jpg"
type DocExt = GetExtension<"report.pdf">; // "pdf"
type ValidImageFile<T extends string> =
GetExtension<T> extends "jpg" | "png" | "gif" | "webp"
? T
: never;
function loadImage<T extends string>(
filename: ValidImageFile<T>
) {
// Only accepts files with valid image extensions
}
loadImage("photo.jpg"); // ✓
loadImage("document.pdf"); // ✗ Type error
Splitting strings on delimiters:
type Split<S extends string, D extends string> =
S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S];
type Path = "users/123/posts";
type Segments = Split<Path, "/">;
// Result: ["users", "123", "posts"]
Building Type-Safe String Builders
Template literal types excel at creating type-safe builders that provide autocomplete and validation.
CSS-in-JS class name builder with spacing utilities:
type SpacingValue = 0 | 1 | 2 | 3 | 4 | 8 | 16;
type SpacingDirection = "t" | "r" | "b" | "l" | "x" | "y";
type SpacingClass = `m${SpacingDirection}-${SpacingValue}` |
`p${SpacingDirection}-${SpacingValue}`;
const classes: SpacingClass[] = [
"mt-4", // margin-top: 1rem
"px-8", // padding-left/right: 2rem
"mb-2" // margin-bottom: 0.5rem
];
Type-safe event emitter:
type EventMap = {
user: { id: string; name: string };
order: { orderId: string; total: number };
payment: { method: string };
};
type EventName = keyof EventMap;
type EventHandler<E extends EventName> =
`on${Capitalize<E>}`;
type EventHandlers = {
[K in EventName as EventHandler<K>]: (data: EventMap[K]) => void;
};
class TypedEmitter implements EventHandlers {
onUser(data: EventMap["user"]) {
console.log(`User: ${data.name}`);
}
onOrder(data: EventMap["order"]) {
console.log(`Order: ${data.orderId}`);
}
onPayment(data: EventMap["payment"]) {
console.log(`Payment: ${data.method}`);
}
}
Real-World Applications
Type-safe environment variables with validation:
type Environment = "development" | "staging" | "production";
type Service = "database" | "redis" | "api";
type EnvVar = `${Uppercase<Environment>}_${Uppercase<Service>}_URL`;
type EnvConfig = {
[K in EnvVar]: string;
};
const env: Partial<EnvConfig> = {
PRODUCTION_DATABASE_URL: process.env.PRODUCTION_DATABASE_URL,
PRODUCTION_REDIS_URL: process.env.PRODUCTION_REDIS_URL,
PRODUCTION_API_URL: process.env.PRODUCTION_API_URL
};
function getEnvVar<T extends EnvVar>(key: T): string {
const value = env[key];
if (!value) throw new Error(`Missing env var: ${key}`);
return value;
}
REST API route type safety:
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiVersion = "v1" | "v2";
type ApiRoute<V extends ApiVersion, M extends HttpMethod, P extends string> =
`${M} /api/${V}/${P}`;
type UserRoutes =
| ApiRoute<"v1", "GET", "users/:id">
| ApiRoute<"v1", "POST", "users">
| ApiRoute<"v2", "GET", "users/:id/profile">;
function defineRoute<T extends UserRoutes>(
route: T,
handler: (req: any) => any
) {
// Route is type-checked at compile time
}
defineRoute("GET /api/v1/users/:id", (req) => {});
defineRoute("POST /api/v3/users", (req) => {}); // ✗ Type error
Performance Considerations and Limitations
Template literal types can cause significant compiler slowdown when overused. The TypeScript compiler has to evaluate every possible combination, and deeply nested or recursive types can create exponential complexity.
Problematic recursive type:
// DON'T: This can cause compiler hangs
type DeepNest<S extends string, N extends number = 10> =
N extends 0
? S
: DeepNest<`wrapper<${S}>`, Subtract<N, 1>>;
Better approach using union distribution:
// DO: Limit recursion depth and use tail recursion
type SafeNest<S extends string, N extends number, Acc extends any[] = []> =
Acc['length'] extends N
? S
: SafeNest<`wrapper<${S}>`, N, [...Acc, any]>;
// Or better yet, avoid deep nesting entirely
type Wrapper<S extends string> = `wrapper<${S}>`;
type DoubleWrapper<S extends string> = Wrapper<Wrapper<S>>;
Key performance guidelines:
- Keep recursion depth under 10 levels
- Use union distribution instead of nested conditionals when possible
- Cache complex types in type aliases
- Avoid template literals in hot paths (frequently instantiated generics)
Conclusion and Best Practices
Template literal types are a powerful tool for building type-safe APIs that feel magical to use. They’re particularly valuable for:
- Domain-specific languages (DSLs) embedded in TypeScript
- API clients where string patterns matter (URLs, GraphQL, SQL)
- Configuration systems requiring specific naming conventions
- Framework code that benefits from compile-time validation
Follow these best practices:
Use template literals for validation, not transformation. While you can build complex string transformations, runtime code is often clearer for actual string manipulation.
Start simple. Basic template literal types are easy to understand. Add complexity only when the type safety benefits justify the maintenance cost.
Document your types. Template literal types can be cryptic. Add JSDoc comments explaining what patterns they match and why.
Test edge cases. Use TypeScript’s type testing utilities or write explicit type assertions to verify your template literal types behave as expected.
Template literal types represent TypeScript’s commitment to pushing type safety into previously dynamic domains. Used judiciously, they eliminate entire classes of runtime errors and create delightful developer experiences with autocomplete and instant feedback.