TypeScript Template Literal Types: Type-Level Strings
Master template literal types to model string patterns, build type-safe APIs around route paths, event names, and CSS values in TypeScript.
What you'll learn
- ✓How template literal types compose at the type level
- ✓Using them with union distribution
- ✓Building event-name and CSS-property unions
- ✓Combining with infer for type-level parsing
- ✓Trade-offs and compile-time cost
Prerequisites
- •TypeScript basics from /blog/typescript-basic-types
- •Generics from /blog/typescript-generics-basics
- •Utility types from /blog/typescript-utility-types
Template literal types let you build string literal types from other string literal types using the same syntax as JavaScript template strings. They make it possible to model precise string patterns at the type level, which is useful for routes, event names, CSS values, and any API where strings carry structured meaning.
The Basics
A template literal type uses backticks and dollar-brace placeholders, just like template strings, but inside type positions.
type Greeting = `Hello, ${string}`;
const a: Greeting = 'Hello, world';
const b: Greeting = 'Goodbye';
// Error: 'Goodbye' is not assignable to type Greeting.
The placeholder accepts any subtype of string, number, bigint, boolean, null, or undefined. When you substitute a specific literal, the result is also a literal.
type Greet = `Hello, ${'Ada' | 'Grace'}`;
// 'Hello, Ada' | 'Hello, Grace'
This distributive behavior is the heart of the feature. When the placeholder is a union, the template literal type expands across every combination.
Combinatorial Power
Multiple union placeholders multiply, producing the Cartesian product of literals.
type Direction = 'top' | 'right' | 'bottom' | 'left';
type Side = 'border' | 'margin' | 'padding';
type SideProp = `${Side}-${Direction}`;
// 'border-top' | 'border-right' | ... | 'padding-left'
This pattern is widely used to model CSS properties without writing them out by hand. The compiler enforces correctness, and autocomplete suggests every valid combination.
Event Names
A common React pattern is to derive event handler names from a base event name. Template literals plus the Capitalize utility produce the convention automatically.
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickHandlers = EventName<'click' | 'hover' | 'focus'>;
// 'onClick' | 'onHover' | 'onFocus'
The built-in string utility types Capitalize, Uncapitalize, Uppercase, and Lowercase work inside template literals to transform substrings.
Route Patterns
Template literal types can model URL patterns and extract their parameters using conditional types with infer. Here is a tiny route param parser.
type ExtractParam<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParam<`/${Rest}`>
: Path extends `${string}:${infer Param}`
? Param
: never;
type UserRoute = '/users/:userId/posts/:postId';
type Params = ExtractParam<UserRoute>;
// 'userId' | 'postId'
This pattern powers type-safe routers like the one in TanStack Router. Once you have the param union, you can build helpers that require callers to supply every parameter without typos.
Building Type-Safe Helpers
Combine the parser with a generic function and the keyof operator to enforce that link generators receive exactly the right arguments.
type ParamsObject<Path extends string> = {
[K in ExtractParam<Path>]: string;
};
function buildPath<P extends string>(path: P, params: ParamsObject<P>): string {
let out: string = path;
for (const key in params) {
out = out.replace(`:${key}`, params[key]);
}
return out;
}
buildPath('/users/:userId/posts/:postId', {
userId: 'u1',
postId: 'p7',
});
Forgetting a required param or supplying an unknown one becomes a compile-time error rather than a runtime bug.
Working With Numbers
You can include number placeholders, but the placeholder accepts the entire number type, not a specific range. To restrict to a finite set, you must enumerate the literals.
type Pixel<N extends number> = `${N}px`;
type Sizes = Pixel<8 | 12 | 16 | 24>;
// '8px' | '12px' | '16px' | '24px'
This is the standard pattern for design tokens, where you want to allow a fixed spacing scale but spell each value as a CSS-friendly string.
Limitations
Template literal types are powerful but not unlimited. They cannot parse arbitrary regular expressions, recurse too deeply without hitting compiler limits, or perform arithmetic on numbers. TypeScript caps recursion depth and union sizes to keep compilation tractable.
In practice you will hit the type instantiation limit if you try to expand Cartesian products of large unions. Splitting the type into smaller helpers and using intermediate aliases usually solves the problem.
Avoid using template literal types for input validation where the input is dynamic at runtime. Types are erased at compile time, so a value that conforms by accident is still accepted at runtime. Use a runtime validator like Zod or a custom regex check when validating user input.
Patterns Worth Knowing
Splitting a string literal type into its parts.
type Split<S extends string, Sep extends string> =
S extends `${infer Head}${Sep}${infer Tail}`
? [Head, ...Split<Tail, Sep>]
: [S];
type Parts = Split<'a.b.c.d', '.'>;
// ['a', 'b', 'c', 'd']
Joining a tuple of strings back together.
type Join<T extends readonly string[], Sep extends string> =
T extends readonly [infer H extends string, ...infer R extends string[]]
? R extends []
? H
: `${H}${Sep}${Join<R, Sep>}`
: '';
type Joined = Join<['a', 'b', 'c'], '-'>;
// 'a-b-c'
These two helpers cover most string manipulation you need at the type level.
Performance Notes
Template literal types and conditional types together are evaluated lazily, but very large unions can slow the compiler considerably. If your IDE feels sluggish on a file that uses heavy type-level string work, look for places where the Cartesian product is unnecessary and use distribution conditionals to narrow earlier.
For more grounding, see /blog/typescript-generics-basics and /blog/typescript-utility-types, both of which combine well with template literal types.
Wrap up
Template literal types let you express string patterns and parse them at the type level, enabling APIs that catch typos at compile time. Combined with conditional types and infer, they form the backbone of modern type-safe libraries for routing, theming, and event handling. Use them for structure, not for runtime validation.