Demonstrates powerful TypeScript template literal types that enforce string patterns at compile time, including event naming conventions, CSS properties, route parameter extraction, and tuple joining.
// 1. EventName<T> - Enforce event naming conventions
type EventActions = 'click' | 'hover' | 'focus' | 'blur';
type EventTargets = 'button' | 'input' | 'link' | 'form';
type EventName<T extends string> = `on${Capitalize<T>}`;
type DOMEventName = EventName<EventActions>;
// Result: 'onClick' | 'onHover' | 'onFocus' | 'onBlur'
type FullEventName = `${EventTargets}_${EventActions}`;
// Result: 'button_click' | 'button_hover' | ... (16 combinations)
function handleEvent(eventName: DOMEventName, callback: () => void): void {
console.log(`Registered handler for: ${eventName}`);
callback();
}
// ✅ Valid - compiles successfully
handleEvent('onClick', () => console.log('Button clicked'));
handleEvent('onFocus', () => console.log('Input focused'));
// ❌ Error: Argument of type '"onclick"' is not assignable
// handleEvent('onclick', () => {});
// ❌ Error: Argument of type '"onScroll"' is not assignable
// handleEvent('onScroll', () => {});
// 2. CSSProperty - Type-safe CSS property access
type CSSUnits = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue<T extends number = number> = `${T}${CSSUnits}`;
type CSSColorProperty = 'color' | 'backgroundColor' | 'borderColor';
type CSSSizeProperty = 'width' | 'height' | 'margin' | 'padding';
type CSSProperty = CSSColorProperty | CSSSizeProperty;
type CSSStyles = {
[K in CSSColorProperty]?: string;
} & {
[K in CSSSizeProperty]?: CSSValue;
};
function applyStyles(element: string, styles: CSSStyles): void {
console.log(`Applying styles to ${element}:`, styles);
}
// ✅ Valid - all values match expected patterns
applyStyles('div', {
width: '100px',
height: '50vh',
backgroundColor: '#ff0000',
margin: '2rem'
});
// ❌ Error: Type '"100"' is not assignable to type CSSValue
// applyStyles('div', { width: '100' });
// ❌ Error: Type '"100pixels"' is not assignable to type CSSValue
// applyStyles('div', { height: '100pixels' });
// 3. RouteParams<T> - Extract parameters from route strings
type ExtractParam<T extends string> =
T extends `:${infer Param}` ? Param : never;
type RouteParams<T extends string> =
T extends `${infer _Start}/:${infer Param}/${infer Rest}`
? Param | RouteParams<`/${Rest}`>
: T extends `${infer _Start}/:${infer Param}`
? Param
: never;
type UserRouteParams = RouteParams<'/users/:userId/posts/:postId'>;
// Result: 'userId' | 'postId'
type ProfileParams = RouteParams<'/profile/:username'>;
// Result: 'username'
function createRoute<T extends string>(
template: T,
params: Record<RouteParams<T>, string>
): string {
let result: string = template;
for (const [key, value] of Object.entries(params)) {
result = result.replace(`:${key}`, value as string);
}
return result;
}
// ✅ Valid - all required params provided
const userPostUrl = createRoute(
'/users/:userId/posts/:postId',
{ userId: '123', postId: '456' }
);
console.log('Generated URL:', userPostUrl);
// Output: '/users/123/posts/456'
// ❌ Error: Property 'postId' is missing
// createRoute('/users/:userId/posts/:postId', { userId: '123' });
// ❌ Error: Object literal may only specify known properties
// createRoute('/users/:userId', { userId: '123', extra: 'value' });
// 4. Join<T, Sep> - Join tuple elements with separator
type Join<T extends readonly string[], Sep extends string> =
T extends readonly []
? ''
: T extends readonly [infer First extends string]
? First
: T extends readonly [infer First extends string, ...infer Rest extends string[]]
? `${First}${Sep}${Join<Rest, Sep>}`
: string;
type PathSegments = ['users', 'profile', 'settings'];
type JoinedPath = Join<PathSegments, '/'>;
// Result: 'users/profile/settings'
type CSSClasses = ['btn', 'btn-primary', 'disabled'];
type ClassString = Join<CSSClasses, ' '>;
// Result: 'btn btn-primary disabled'
function joinStrings<T extends readonly string[], Sep extends string>(
items: T,
separator: Sep
): Join<T, Sep> {
return items.join(separator) as Join<T, Sep>;
}
// ✅ Type is exactly 'a-b-c', not just string
const kebabCase = joinStrings(['a', 'b', 'c'] as const, '-');
console.log('Kebab case:', kebabCase);
const cssClass = joinStrings(['container', 'flex', 'center'] as const, ' ');
console.log('CSS classes:', cssClass);
// 5. Advanced: API Endpoint Builder
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type APIVersion = 'v1' | 'v2' | 'v3';
type Resource = 'users' | 'posts' | 'comments';
type APIEndpoint = `/api/${APIVersion}/${Resource}`;
type FullAPIRoute<M extends HTTPMethod, E extends APIEndpoint> = `${M} ${E}`;
type ValidRoutes = FullAPIRoute<HTTPMethod, APIEndpoint>;
function registerEndpoint<M extends HTTPMethod, E extends APIEndpoint>(
method: M,
endpoint: E,
handler: () => void
): FullAPIRoute<M, E> {
const route = `${method} ${endpoint}` as FullAPIRoute<M, E>;
console.log(`Registered: ${route}`);
handler();
return route;
}
// ✅ Valid endpoints
registerEndpoint('GET', '/api/v1/users', () => console.log('Fetching users'));
registerEndpoint('POST', '/api/v2/posts', () => console.log('Creating post'));
// ❌ Error: Argument of type '"/api/v4/users"' is not assignable
// registerEndpoint('GET', '/api/v4/users', () => {});
// ❌ Error: Argument of type '"REMOVE"' is not assignable to type HTTPMethod
// registerEndpoint('REMOVE', '/api/v1/users', () => {});
console.log('\n✅ All template literal type examples executed successfully!');Template literal types, introduced in TypeScript 4.1, allow you to build string types from other types using the same syntax as JavaScript template literals. This enables powerful compile-time validation of string patterns that would otherwise require runtime checks or remain unchecked entirely.
The EventName<T> example demonstrates how to enforce naming conventions. By using the built-in Capitalize utility type within a template literal, we ensure that all event names follow the 'onAction' pattern. The compiler rejects 'onclick' (wrong casing) or 'onScroll' (not in our allowed actions). This pattern is invaluable for maintaining consistency across large codebases and catching typos before they become runtime bugs.
The RouteParams<T> type showcases conditional type inference combined with template literals. Using the infer keyword, we recursively extract parameter names from route strings like '/users/:userId/posts/:postId'. The resulting union type ('userId' | 'postId') is then used to enforce that the params object contains exactly the required keys. This eliminates an entire class of bugs where developers forget to provide route parameters or misspell them.
The Join<T, Sep> type demonstrates recursive type computation. It processes a tuple of strings and produces a single literal type representing them joined by a separator. The recursion handles the base cases (empty tuple, single element) and the recursive case (join first element with the rest). When used with as const assertions, the return type is the exact joined string, not just 'string', enabling further type-safe operations.
Use template literal types when you need to validate string patterns at compile time: API routes, event names, CSS class names, SQL queries, or any domain-specific string formats. Avoid them for highly dynamic strings where the pattern isn't known at compile time, or when the combinatorial explosion of possible types would slow down type checking (e.g., joining arbitrary-length arrays produces exponentially complex types). For best results, combine template literal types with discriminated unions and conditional types to build comprehensive type-safe string APIs.