Back to Blog
TypeScriptTemplate LiteralsType SystemAdvanced Types

TypeScript Template Literal Types

Master template literal types in TypeScript. From string patterns to event handlers to API routes.

B
Bootspring Team
Engineering
March 13, 2021
6 min read

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'; // Error

Event 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.

Share this article

Help spread the word about Bootspring