Template literal types create string patterns at the type level. Here's how to use them effectively.
Basic Template Literals#
1// Simple concatenation
2type Greeting = `Hello, ${string}`;
3
4const valid: Greeting = 'Hello, World';
5const alsoValid: Greeting = 'Hello, TypeScript';
6// const invalid: Greeting = 'Hi, World'; // Error
7
8// With union types
9type Color = 'red' | 'green' | 'blue';
10type Size = 'small' | 'medium' | 'large';
11
12type ColorSize = `${Color}-${Size}`;
13// 'red-small' | 'red-medium' | 'red-large' |
14// 'green-small' | 'green-medium' | 'green-large' |
15// 'blue-small' | 'blue-medium' | 'blue-large'
16
17const shirt: ColorSize = 'blue-medium'; // OK
18// const invalid: ColorSize = 'yellow-small'; // ErrorEvent Handler Types#
1// DOM event handlers
2type EventName = 'click' | 'focus' | 'blur' | 'change';
3type HandlerName = `on${Capitalize<EventName>}`;
4// 'onClick' | 'onFocus' | 'onBlur' | 'onChange'
5
6// Custom event system
7type Events = 'user:login' | 'user:logout' | 'cart:add' | 'cart:remove';
8
9type EventHandler<E extends Events> = (event: E) => void;
10
11function on<E extends Events>(event: E, handler: EventHandler<E>) {
12 // ...
13}
14
15on('user:login', (event) => {
16 // event is typed as 'user:login'
17});
18
19// Generate listener names
20type ListenerName<E extends string> = `on${Capitalize<E>}`;
21
22type UserEvents = ListenerName<'login' | 'logout'>;
23// 'onLogin' | 'onLogout'CSS Property Types#
1// CSS units
2type Unit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
3type CSSValue = `${number}${Unit}`;
4
5const width: CSSValue = '100px';
6const height: CSSValue = '50vh';
7// const invalid: CSSValue = '10'; // Error - needs unit
8
9// CSS properties
10type CSSProperty = 'margin' | 'padding' | 'border';
11type Direction = 'top' | 'right' | 'bottom' | 'left';
12
13type DirectionalProperty = `${CSSProperty}-${Direction}`;
14// 'margin-top' | 'margin-right' | ... | 'border-left'
15
16// Color formats
17type HexColor = `#${string}`;
18type RGBColor = `rgb(${number}, ${number}, ${number})`;
19type RGBAColor = `rgba(${number}, ${number}, ${number}, ${number})`;
20
21type CSSColor = HexColor | RGBColor | RGBAColor;
22
23const hex: CSSColor = '#ff0000';
24const rgb: CSSColor = 'rgb(255, 0, 0)';API Route Types#
1// REST API routes
2type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
3type ApiVersion = 'v1' | 'v2';
4type Resource = 'users' | 'posts' | 'comments';
5
6type ApiRoute = `/api/${ApiVersion}/${Resource}`;
7// '/api/v1/users' | '/api/v1/posts' | ... | '/api/v2/comments'
8
9// Dynamic routes
10type DynamicRoute<T extends string> = `/api/${T}/:id`;
11
12type UserRoute = DynamicRoute<'users'>;
13// '/api/users/:id'
14
15// Route with query params
16type QueryParam = `${string}=${string}`;
17type RouteWithQuery<R extends string> = `${R}?${QueryParam}`;
18
19// Extract route params
20type ExtractParams<T extends string> =
21 T extends `${string}:${infer Param}/${infer Rest}`
22 ? Param | ExtractParams<Rest>
23 : T extends `${string}:${infer Param}`
24 ? Param
25 : never;
26
27type Params = ExtractParams<'/users/:userId/posts/:postId'>;
28// 'userId' | 'postId'Getter/Setter Generation#
1// Generate getter/setter types from properties
2type Getters<T> = {
3 [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
4};
5
6type Setters<T> = {
7 [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
8};
9
10interface Person {
11 name: string;
12 age: number;
13}
14
15type PersonGetters = Getters<Person>;
16// { getName: () => string; getAge: () => number; }
17
18type PersonSetters = Setters<Person>;
19// { setName: (value: string) => void; setAge: (value: number) => void; }
20
21// Combined
22type PersonMethods = Getters<Person> & Setters<Person>;String Manipulation Types#
1// Built-in utilities
2type Upper = Uppercase<'hello'>; // 'HELLO'
3type Lower = Lowercase<'HELLO'>; // 'hello'
4type Cap = Capitalize<'hello'>; // 'Hello'
5type Uncap = Uncapitalize<'Hello'>; // 'hello'
6
7// Kebab case to camel case
8type KebabToCamel<S extends string> =
9 S extends `${infer First}-${infer Rest}`
10 ? `${First}${Capitalize<KebabToCamel<Rest>>}`
11 : S;
12
13type CamelCase = KebabToCamel<'background-color'>;
14// 'backgroundColor'
15
16// Split string
17type Split<S extends string, D extends string> =
18 S extends `${infer Head}${D}${infer Tail}`
19 ? [Head, ...Split<Tail, D>]
20 : S extends ''
21 ? []
22 : [S];
23
24type Parts = Split<'a-b-c', '-'>;
25// ['a', 'b', 'c']
26
27// Join array to string
28type Join<T extends string[], D extends string> =
29 T extends []
30 ? ''
31 : T extends [infer First extends string]
32 ? First
33 : T extends [infer First extends string, ...infer Rest extends string[]]
34 ? `${First}${D}${Join<Rest, D>}`
35 : never;
36
37type Joined = Join<['a', 'b', 'c'], '-'>;
38// 'a-b-c'Configuration Keys#
1// Environment variables
2type EnvPrefix = 'NEXT_PUBLIC' | 'REACT_APP';
3type EnvName = 'API_URL' | 'API_KEY' | 'DEBUG';
4
5type EnvVar = `${EnvPrefix}_${EnvName}`;
6// 'NEXT_PUBLIC_API_URL' | 'NEXT_PUBLIC_API_KEY' | ...
7
8// Feature flags
9type Feature = 'dark_mode' | 'new_ui' | 'beta_features';
10type FeatureFlag = `FEATURE_${Uppercase<Feature>}`;
11// 'FEATURE_DARK_MODE' | 'FEATURE_NEW_UI' | 'FEATURE_BETA_FEATURES'
12
13// Localization keys
14type Namespace = 'common' | 'auth' | 'dashboard';
15type Key = 'title' | 'description' | 'button';
16
17type I18nKey = `${Namespace}.${Key}`;
18// 'common.title' | 'common.description' | ... | 'dashboard.button'Path Types#
1// File paths
2type Extension = 'ts' | 'tsx' | 'js' | 'jsx';
3type FileName = `${string}.${Extension}`;
4
5const file: FileName = 'index.tsx';
6// const invalid: FileName = 'index.py'; // Error
7
8// Nested paths
9type NestedKey<T, P extends string = ''> = T extends object
10 ? {
11 [K in keyof T]: K extends string
12 ? P extends ''
13 ? K | NestedKey<T[K], K>
14 : `${P}.${K}` | NestedKey<T[K], `${P}.${K}`>
15 : never;
16 }[keyof T]
17 : never;
18
19interface Config {
20 server: {
21 host: string;
22 port: number;
23 };
24 database: {
25 url: string;
26 };
27}
28
29type ConfigPath = NestedKey<Config>;
30// 'server' | 'server.host' | 'server.port' | 'database' | 'database.url'SQL Query Types#
1// Table names
2type Table = 'users' | 'posts' | 'comments';
3
4// SELECT query
5type SelectQuery<T extends Table> = `SELECT * FROM ${T}`;
6
7const query: SelectQuery<'users'> = 'SELECT * FROM users';
8
9// JOIN query
10type JoinQuery<T1 extends Table, T2 extends Table> =
11 `SELECT * FROM ${T1} JOIN ${T2}`;
12
13// Column references
14type Column<T extends Table> = `${T}.id` | `${T}.created_at`;
15
16type UserColumn = Column<'users'>;
17// 'users.id' | 'users.created_at'Validation Patterns#
1// UUID pattern (simplified)
2type UUID = `${string}-${string}-${string}-${string}-${string}`;
3
4const uuid: UUID = '123e4567-e89b-12d3-a456-426614174000';
5
6// Email pattern (simplified)
7type Email = `${string}@${string}.${string}`;
8
9const email: Email = 'user@example.com';
10
11// Semantic version
12type SemVer = `${number}.${number}.${number}`;
13
14const version: SemVer = '1.2.3';
15
16// Date format
17type DateFormat = `${number}-${number}-${number}`;
18
19const date: DateFormat = '2024-01-15';Real-World Example: Router#
1// Type-safe router
2type Routes = {
3 '/': {};
4 '/users': {};
5 '/users/:id': { id: string };
6 '/posts/:postId/comments/:commentId': {
7 postId: string;
8 commentId: string;
9 };
10};
11
12type RoutePath = keyof Routes;
13
14type RouteParams<T extends RoutePath> = Routes[T];
15
16function navigate<T extends RoutePath>(
17 path: T,
18 params: RouteParams<T>
19): void {
20 // Implementation
21}
22
23navigate('/', {});
24navigate('/users/:id', { id: '123' });
25navigate('/posts/:postId/comments/:commentId', {
26 postId: '1',
27 commentId: '2',
28});Best Practices#
Usage:
✓ Event handler names
✓ API route patterns
✓ Configuration keys
✓ CSS property generation
Performance:
✓ Limit union size (avoid combinatorial explosion)
✓ Use constraints to narrow types
✓ Break complex types into smaller parts
✓ Test with specific inputs first
Avoid:
✗ Overly complex recursive types
✗ Very large union generations
✗ Runtime string validation replacement
✗ Deeply nested template types
Conclusion#
Template literal types enable string pattern validation at compile time. Use them for event names, API routes, configuration keys, and generated property names. Be mindful of type complexity to avoid slow compilation.