📚 5 min read
Template Literals in TypeScript ​
This section explores TypeScript's template literal types and their applications in type-safe string manipulation.
Overview ​
Template literal types combine literal types and string manipulation to create powerful type-safe string patterns.
Basic Template Literals ​
String Literals ​
typescript
type Greeting = 'Hello';
type Name = 'World';
type Message = `${Greeting}, ${Name}!`; // type is "Hello, World!"
type ID = `user_${number}`; // type is `user_${number}`
type Status = `${boolean}_status`; // type is "true_status" | "false_status"
// Usage
let message: Message = 'Hello, World!'; // OK
let id: ID = 'user_123'; // OK
let status: Status = 'true_status'; // OK
Union Types in Template Literals ​
typescript
type Color = 'red' | 'blue' | 'green';
type Size = 'small' | 'medium' | 'large';
type ColorSize = `${Color}-${Size}`; // All combinations
// Result type is:
// "red-small" | "red-medium" | "red-large" |
// "blue-small" | "blue-medium" | "blue-large" |
// "green-small" | "green-medium" | "green-large"
type Status = 'success' | 'error' | 'pending';
type EventName = `on${Capitalize<Status>}`; // "onSuccess" | "onError" | "onPending"
Advanced Patterns ​
Intrinsic String Manipulation Types ​
typescript
type Greeting = 'hello world';
type Caps = Uppercase<Greeting>; // "HELLO WORLD"
type Lower = Lowercase<Greeting>; // "hello world"
type Cap = Capitalize<Greeting>; // "Hello world"
type Uncap = Uncapitalize<Greeting>; // "hello world"
// Combining manipulations
type EventHandler<T extends string> = `on${Capitalize<T>}`;
type MouseEvents = 'click' | 'mouseup' | 'mousedown';
type MouseHandlers = EventHandler<MouseEvents>;
// "onClick" | "onMouseup" | "onMousedown"
Pattern Matching with Template Literals ​
typescript
type PropEventSource<Type> = {
on<Key extends string & keyof Type>(
eventName: `${Key}Changed`,
callback: (newValue: Type[Key]) => void
): void;
};
declare function makeWatchedObject<Type>(
obj: Type
): Type & PropEventSource<Type>;
// Usage
const person = makeWatchedObject({
firstName: 'John',
lastName: 'Doe',
age: 30,
});
// OK
person.on('firstNameChanged', (newName) => {
console.log(`new name is ${newName.toUpperCase()}`);
});
// Error: 'firstName' is not 'firstNameChanged'
person.on('firstName', () => {});
// Error: 'age' is number, not string
person.on('ageChanged', (newAge) => {
console.log(`new age is ${newAge.toUpperCase()}`);
});
Real-World Example ​
typescript
// API Route Type Generator
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Version = 'v1' | 'v2';
// Base path generator
type APIPath<V extends Version, Resource extends string> =
`/api/${V}/${Resource}`;
// Route parameter types
type ID = string | number;
type QueryParam = string | number | boolean;
type QueryParams = Record<string, QueryParam>;
// Route builder types
type RouteWithID<Route extends string> = `${Route}/${ID}`;
type RouteWithAction<Route extends string, Action extends string> =
`${Route}/${Action}`;
// API endpoint configuration
interface EndpointConfig<
Method extends HTTPMethod,
Path extends string,
Req = unknown,
Res = unknown
> {
method: Method;
path: Path;
request?: Req;
response?: Res;
}
// API implementation
class APIBuilder<V extends Version> {
private version: V;
private endpoints: Map<string, EndpointConfig<any, any>> = new Map();
constructor(version: V) {
this.version = version;
}
addEndpoint<
Method extends HTTPMethod,
Resource extends string,
Path extends APIPath<V, Resource>,
Req = unknown,
Res = unknown
>(config: EndpointConfig<Method, Path, Req, Res>) {
this.endpoints.set(config.path, config);
return this;
}
getEndpoint<Path extends string>(
path: Path
): EndpointConfig<HTTPMethod, Path> | undefined {
return this.endpoints.get(path);
}
}
// Domain types
interface User {
id: number;
name: string;
email: string;
}
interface CreateUserRequest {
name: string;
email: string;
password: string;
}
interface UpdateUserRequest {
name?: string;
email?: string;
}
// API routes type
type UserRoutes = {
base: APIPath<'v1', 'users'>;
withId: RouteWithID<APIPath<'v1', 'users'>>;
action: RouteWithAction<APIPath<'v1', 'users'>, 'verify'>;
};
// API configuration
const api = new APIBuilder('v1')
.addEndpoint({
method: 'GET',
path: '/api/v1/users',
response: User[]
})
.addEndpoint({
method: 'POST',
path: '/api/v1/users',
request: CreateUserRequest,
response: User
})
.addEndpoint({
method: 'GET',
path: '/api/v1/users/123',
response: User
})
.addEndpoint({
method: 'PUT',
path: '/api/v1/users/123',
request: UpdateUserRequest,
response: User
})
.addEndpoint({
method: 'DELETE',
path: '/api/v1/users/123'
})
.addEndpoint({
method: 'POST',
path: '/api/v1/users/verify',
request: { token: string },
response: { verified: boolean }
});
// Type-safe route builder
function createRoute<
V extends Version,
Resource extends string
>(version: V, resource: Resource): APIPath<V, Resource> {
return `/api/${version}/${resource}`;
}
function withId<Route extends string>(
route: Route,
id: ID
): RouteWithID<Route> {
return `${route}/${id}`;
}
function withAction<Route extends string, Action extends string>(
route: Route,
action: Action
): RouteWithAction<Route, Action> {
return `${route}/${action}`;
}
// Usage
const usersRoute = createRoute('v1', 'users');
const userRoute = withId(usersRoute, 123);
const verifyRoute = withAction(usersRoute, 'verify');
// Type-safe API client
async function fetchAPI<
Method extends HTTPMethod,
Path extends string
>(
config: EndpointConfig<Method, Path>
): Promise<unknown> {
const response = await fetch(config.path, {
method: config.method,
headers: {
'Content-Type': 'application/json'
},
body: config.request ? JSON.stringify(config.request) : undefined
});
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
return config.response ? response.json() : undefined;
}
// Example usage
async function example() {
const endpoint = api.getEndpoint('/api/v1/users/123');
if (endpoint) {
const result = await fetchAPI(endpoint);
console.log(result);
}
}
Best Practices ​
Type Design:
- Keep template literals simple
- Use union types effectively
- Consider type inference
Pattern Matching:
- Use specific patterns
- Handle edge cases
- Validate inputs
Performance:
- Avoid complex unions
- Cache type computations
- Use type aliases