You can do string manipulation at the TYPE level. And it's not just clever — it's useful.
Template literal types are TypeScript's way of applying JavaScript's template literal syntax to the type system. They let you construct string types from other types, split strings into parts, and transform string unions — all at compile time. If you've ever wanted type-safe routing, event names that autocomplete, or CSS-in-JS with full type checking, template literal types are the answer.
The Syntax: Template Literals in Type Position
You already know template literals from JavaScript:
const greeting = `Hello, ${name}!`;
Template literal types use the same syntax, but in type position:
type Greeting = `Hello, ${string}!`;
// Greeting is any string that starts with "Hello, " and ends with "!"
const valid: Greeting = "Hello, World!"; // OK
const invalid: Greeting = "Hi there!"; // Error
The ${string} inside a template literal type is a placeholder that matches any string. You can use string, number, boolean, bigint, null, undefined, or any literal type.
Building String Unions
The simplest use of template literal types is constructing string unions:
type EventName = "click" | "focus" | "blur";
type ElementName = "button" | "input" | "form";
type ElementEvent = `${ElementName}:${EventName}`;
// "button:click" | "button:focus" | "button:blur" |
// "input:click" | "input:focus" | "input:blur" |
// "form:click" | "form:focus" | "form:blur"
TypeScript generates the Cartesian product — every combination of ElementName and EventName. This is incredibly powerful for generating precise string types from smaller building blocks.
Type-Safe Routing
This is where template literal types shine. You can define type-safe routes:
type Resource = "users" | "products" | "orders";
type Action = "list" | "get" | "create" | "update" | "delete";
type Route = `/${Resource}` | `/${Resource}/${Action}` | `/${Resource}/${number}`;
function navigate(route: Route): void {
// ...
}
navigate("/users"); // OK
navigate("/users/list"); // OK
navigate("/users/42"); // OK
navigate("/users/edit"); // Error — 'edit' is not a valid action
navigate("/admin"); // Error — 'admin' is not a valid resource
You can make it even more precise with parameter extraction:
type ExtractRouteParams<T extends string> =
T extends `${infer Start}/${infer Param}/${infer Rest}`
? { [K in Param]: string } & ExtractRouteParams<`/${Rest}`>
: T extends `${infer Start}/${infer Param}`
? { [K in Param]: string }
: {};
type UserRouteParams = ExtractRouteParams<"/users/:id/posts/:postId">;
// { id: string; postId: string; }
String Manipulation Utility Types
TypeScript ships with four built-in string manipulation types:
// Uppercase: Convert to uppercase
type Loud = Uppercase<"hello">; // "HELLO"
// Lowercase: Convert to lowercase
type Quiet = Lowercase<"HELLO">; // "hello"
// Capitalize: Capitalize the first letter
type Proper = Capitalize<"hello">; // "Hello"
// Uncapitalize: Lowercase the first letter
type Improper = Uncapitalize<"Hello">; // "hello"
These work on string literals AND on unions:
type Events = "click" | "focus" | "blur";
type HandlerNames = `on${Capitalize<Events>}`;
// "onClick" | "onFocus" | "onBlur"
This is the standard pattern for generating event handler names from event names.
Practical Pattern: Type-Safe Event Emitters
Combine template literal types with generics for a fully type-safe event system:
type EventMap = {
userCreated: { name: string; email: string };
userDeleted: { id: number };
paymentReceived: { amount: number; currency: string };
};
type EventName = keyof EventMap;
type EventHandlerName = `on${Capitalize<EventName>}`;
// "onUserCreated" | "onUserDeleted" | "onPaymentReceived"
type EventHandlers = {
[K in EventName as `on${Capitalize<K>}`]: (data: EventMap[K]) => void;
};
// {
// onUserCreated: (data: { name: string; email: string }) => void;
// onUserDeleted: (data: { id: number }) => void;
// onPaymentReceived: (data: { amount: number; currency: string }) => void;
// }
Practical Pattern: CSS-in-JS Type Safety
Template literal types can make CSS-in-JS libraries type-safe:
type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";
type Property = "color" | "backgroundColor" | "fontSize";
type CSSProperty = `${Property}-${Color | Size}`;
// Not quite right — we want to constrain which values go with which properties
// Better: use a mapped type
type CSSProperties = {
color: Color;
backgroundColor: Color;
fontSize: Size;
};
type CSSValue<K extends keyof CSSProperties> = `${K}: ${CSSProperties[K]}`;
type ColorRule = CSSValue<"color">; // "color: red" | "color: blue" | "color: green"
type SizeRule = CSSValue<"fontSize">; // "fontSize: small" | "fontSize: medium" | "fontSize: large"
Practical Pattern: Type-Safe i18n Keys
Internationalization keys often follow patterns like namespace:section.key. Template literal types can make them type-safe:
type Namespace = "common" | "auth" | "dashboard";
type Section = "button" | "label" | "message";
type Key = "save" | "cancel" | "delete" | "confirm";
type I18nKey = `${Namespace}.${Section}.${Key}`;
// "common.button.save" | "common.button.cancel" | ... (48 combinations)
function translate(key: I18nKey): string {
return translations[key];
}
translate("auth.button.save"); // OK
translate("auth.button.submit"); // Error — 'submit' is not a valid key
Extracting Parts of String Literals
Template literal types work in both directions — you can construct strings AND deconstruct them using infer:
type Split<S extends string> = S extends `${infer First}.${infer Rest}` ? [First, ...Split<Rest>] : [S];
type Parts = Split<"auth.button.save">; // ["auth", "button", "save"]
This is a recursive type that splits a string on . and returns a tuple of parts.
Extracting Specific Patterns
// Extract a prefix
type RemovePrefix<S extends string, Prefix extends string> =
S extends `${Prefix}${infer Rest}` ? Rest : S;
type A = RemovePrefix<"prefix_user", "prefix_">; // "user"
type B = RemovePrefix<"user", "prefix_">; // "user" (no match — returns original)
// Extract between delimiters
type Between<S extends string, Left extends string, Right extends string> =
S extends `${Left}${infer Middle}${Right}` ? Middle : never;
type C = Between<"[hello]", "[", "]">; // "hello"
type D = Between<"no brackets", "[", "]">; // never
Recursive Template Literal Types
Combine template literal types with recursion for powerful string transformations:
// Convert snake_case to camelCase
type SnakeToCamel<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<SnakeToCamel<Tail>>}`
: S;
type Camel = SnakeToCamel<"user_first_name">; // "userFirstName"
// Convert kebab-case to PascalCase
type KebabToPascal<S extends string> =
S extends `${infer Head}-${infer Tail}`
? `${Capitalize<Head>}${KebabToPascal<Tail>}`
: Capitalize<S>;
type Pascal = KebabToPascal<"user-first-name">; // "UserFirstName"
// Join a tuple of strings with a delimiter
type Join<T extends string[], D extends string> =
T extends [infer First extends string, ...infer Rest extends string[]]
? Rest extends []
? First
: `${First}${D}${Join<Rest, D>}`
: "";
type Joined = Join<["a", "b", "c"], "-">; // "a-b-c"
The Intrinsic String Types
TypeScript has four built-in intrinsic string manipulation types: Uppercase, Lowercase, Capitalize, and Uncapitalize. These are implemented in the compiler itself (not as user-land types) and are the only types that can actually transform string literals at the character level.
You can use them in combination with template literal types:
type TitleCase<S extends string> =
S extends `${infer First} ${infer Rest}`
? `${Capitalize<First>} ${TitleCase<Rest>}`
: Capitalize<S>;
type Title = TitleCase<"the quick brown fox">; // "The Quick Brown Fox"
Performance Considerations
Template literal types are powerful but can be expensive. The compiler limits:
- Union combinations: if you cross two unions with 10,000+ combinations, TypeScript may slow down or error
- Recursion depth: recursive template literal types are limited to ~50 levels
- Complex unions: avoid generating unions with thousands of members
For most real-world use cases, these limits are far beyond what you'll hit. But if you're generating types for a routing library with hundreds of routes, be aware of the limits.
Try This: Template Literal Types
- Create a
type Greeting = `Hello, ${string}!and try assigning various strings to it. - Create a union of event names and element names. Generate all combinations with template literal types.
- Write a
SnakeToCamel<T>type that converts"user_first_name"to"userFirstName". - Write a
KebabToPascal<T>type that converts"my-component"to"MyComponent". - Create a type-safe route builder that generates routes from resource and action unions.
- Write a
Split<S, D>type that splits a string on a delimiter and returns a tuple.
Time needed: 25 minutes.
What to notice: How template literal types turn string manipulation from a runtime concern into a compile-time guarantee. How recursive types let you process strings of arbitrary length. How the combination of template literal types and infer gives you a complete string processing toolkit at the type level.
The Revelation
You now know how to manipulate strings at the type level. Combined with infer from the last chapter, you can extract, transform, and generate types from patterns. But there's one more fundamental type manipulation technique: mapped types.
Mapped types let you transform EVERY property of an object type at once — making them optional, readonly, or transforming their types. They're the foundation of Partial, Required, Readonly, and every other object-transforming utility type.
That's the next chapter. And it's where TypeScript's type system goes from "expressive" to "truly programmable."
In the next chapter: mapped types, key remapping, the as clause, and how to transform entire object types with a single line of type code.