Advanced TypeScript Patterns That Will Make You a Better JavaScript Developer
TypeScript has revolutionized JavaScript development by bringing strong typing and advanced patterns to the ecosystem. In this guide, we’ll explore advanced TypeScript patterns that not only make your code more robust but also improve your overall JavaScript development skills.
Table of Contents
- Type-Level Programming
- Advanced Generic Patterns
- Discriminated Unions
- Utility Types Deep Dive
- Factory Patterns
- State Management Patterns
- Builder Patterns
- Real-World Examples
1. Type-Level Programming
Conditional Types
// Advanced conditional type pattern
type IsArray<T> = T extends Array<any> ? true : false;
type IsString<T> = T extends string ? true : false;
// Practical example: API response handler
type ApiResponse<T> = {
data: T;
status: number;
message: string;
};
type ExtractData<T> = T extends ApiResponse<infer U> ? U : never;
// Usage example
interface UserData {
id: number;
name: string;
}
type UserApiResponse = ApiResponse<UserData>;
type ExtractedUserData = ExtractData<UserApiResponse>; // Returns UserData type
Template Literal Types
// Define valid HTTP methods
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiEndpoint = '/users' | '/posts' | '/comments';
// Create API route types
type ApiRoute = `${HttpMethod} ${ApiEndpoint}`;
// Validate routes at compile time
const validRoute: ApiRoute = 'GET /users'; // ✅ Valid
const invalidRoute: ApiRoute = 'PATCH /users'; // ❌ Type error
2. Advanced Generic Patterns
Factory with Generic Constraints
interface HasId {
id: number;
}
interface HasTimestamps {
createdAt: Date;
updatedAt: Date;
}
// Generic factory with constraints
class EntityFactory<T extends HasId & HasTimestamps> {
create(data: Omit<T, keyof HasTimestamps>): T {
return {
...data,
createdAt: new Date(),
updatedAt: new Date()
} as T;
}
update(entity: T, data: Partial<Omit<T, keyof HasId | keyof HasTimestamps>>): T {
return {
...entity,
...data,
updatedAt: new Date()
};
}
}
// Usage example
interface User extends HasId, HasTimestamps {
id: number;
name: string;
email: string;
}
const userFactory = new EntityFactory<User>();
const user = userFactory.create({ id: 1, name: 'John', email: 'john@example.com' });
Type-Safe Event Emitter
type EventMap = {
'user:login': { userId: string; timestamp: number };
'user:logout': { userId: string; timestamp: number };
'error': { message: string; code: number };
}
class TypedEventEmitter<T extends Record<string, any>> {
private listeners: Partial<Record<keyof T, Function[]>> = {};
on<K extends keyof T>(event: K, callback: (data: T[K]) => void) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]?.push(callback);
}
emit<K extends keyof T>(event: K, data: T[K]) {
this.listeners[event]?.forEach(callback => callback(data));
}
}
// Usage
const emitter = new TypedEventEmitter<EventMap>();
emitter.on('user:login', ({ userId, timestamp }) => {
console.log(`User ${userId} logged in at ${timestamp}`);
});
// Type-safe emit
emitter.emit('user:login', {
userId: '123',
timestamp: Date.now()
});
3. Discriminated Unions
State Management Pattern
// Define possible states with discriminated union
type AsyncState<T, E = Error> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: E };
// Generic async data handler
class AsyncData<T, E = Error> {
private state: AsyncState<T, E> = { status: 'idle' };
setState(newState: AsyncState<T, E>) {
this.state = newState;
this.render();
}
render() {
switch (this.state.status) {
case 'idle':
console.log('Waiting to start...');
break;
case 'loading':
console.log('Loading...');
break;
case 'success':
console.log('Data:', this.state.data);
break;
case 'error':
console.log('Error:', this.state.error);
break;
}
}
}
// Usage
interface UserProfile {
id: string;
name: string;
}
const userProfile = new AsyncData<UserProfile>();
4. Utility Types Deep Dive
Advanced Mapped Types
// Make all properties optional and nullable
type Nullable<T> = { [P in keyof T]: T[P] | null };
// Make specific properties required
type RequiredProps<T, K extends keyof T> = T & { [P in K]-?: T[P] };
// Deep partial type
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Usage example
interface Config {
api: {
baseUrl: string;
timeout: number;
headers: {
authorization: string;
contentType: string;
};
};
cache: {
enabled: boolean;
ttl: number;
};
}
type PartialConfig = DeepPartial<Config>;
const config: PartialConfig = {
api: {
baseUrl: 'https://api.example.com',
headers: {
authorization: 'Bearer token'
}
}
};
5. Factory Patterns
Abstract Factory Pattern
// Abstract product interfaces
interface Button {
render(): void;
onClick(): void;
}
interface Input {
render(): void;
getValue(): string;
}
// Abstract factory interface
interface UIFactory {
createButton(): Button;
createInput(): Input;
}
// Concrete implementations
class MaterialButton implements Button {
render() { console.log('Rendering Material button'); }
onClick() { console.log('Material button clicked'); }
}
class MaterialInput implements Input {
render() { console.log('Rendering Material input'); }
getValue() { return 'Material input value'; }
}
class MaterialUIFactory implements UIFactory {
createButton(): Button {
return new MaterialButton();
}
createInput(): Input {
return new MaterialInput();
}
}
// Usage with dependency injection
class Form {
constructor(private factory: UIFactory) {}
render() {
const button = this.factory.createButton();
const input = this.factory.createInput();
button.render();
input.render();
}
}
6. State Management Patterns
Type-Safe Redux Pattern
// Action types with discriminated unions
type Action =
| { type: 'ADD_TODO'; payload: { text: string } }
| { type: 'TOGGLE_TODO'; payload: { id: number } }
| { type: 'DELETE_TODO'; payload: { id: number } };
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface State {
todos: Todo[];
loading: boolean;
}
// Type-safe reducer
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, {
id: Date.now(),
text: action.payload.text,
completed: false
}]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload.id)
};
}
}
7. Builder Patterns
Fluent Builder Pattern
class QueryBuilder<T> {
private query: Partial<T> = {};
private conditions: Array<(item: T) => boolean> = [];
where<K extends keyof T>(key: K, value: T[K]): this {
this.query[key] = value;
return this;
}
whereIn<K extends keyof T>(key: K, values: T[K][]): this {
this.conditions.push((item: T) =>
values.includes(item[key])
);
return this;
}
build(): (item: T) => boolean {
const query = this.query;
const conditions = this.conditions;
return (item: T) => {
const matchesQuery = Object.entries(query).every(
([key, value]) => item[key as keyof T] === value
);
const matchesConditions = conditions.every(
condition => condition(item)
);
return matchesQuery && matchesConditions;
};
}
}
// Usage example
interface User {
id: number;
name: string;
age: number;
role: 'admin' | 'user';
}
const query = new QueryBuilder<User>()
.where('role', 'admin')
.whereIn('age', [25, 30, 35])
.build();
const users: User[] = [
{ id: 1, name: 'John', age: 30, role: 'admin' },
{ id: 2, name: 'Jane', age: 25, role: 'user' }
];
const results = users.filter(query);
8. Real-World Examples
Type-Safe API Client
// API endpoints definition
interface ApiEndpoints {
'/users': {
GET: {
response: User[];
query: { role?: string };
};
POST: {
body: Omit<User, 'id'>;
response: User;
};
};
'/users/:id': {
GET: {
params: { id: string };
response: User;
};
PUT: {
params: { id: string };
body: Partial<User>;
response: User;
};
};
}
// Type-safe API client
class ApiClient {
async get<
Path extends keyof ApiEndpoints,
Method extends keyof ApiEndpoints[Path],
Endpoint extends ApiEndpoints[Path][Method]
>(
path: Path,
config?: {
params?: Endpoint extends { params: any } ? Endpoint['params'] : never;
query?: Endpoint extends { query: any } ? Endpoint['query'] : never;
}
): Promise<Endpoint extends { response: any } ? Endpoint['response'] : never> {
// Implementation
return {} as any;
}
async post<
Path extends keyof ApiEndpoints,
Method extends keyof ApiEndpoints[Path],
Endpoint extends ApiEndpoints[Path][Method]
>(
path: Path,
body: Endpoint extends { body: any } ? Endpoint['body'] : never
): Promise<Endpoint extends { response: any } ? Endpoint['response'] : never> {
// Implementation
return {} as any;
}
}
// Usage
const api = new ApiClient();
// Type-safe API calls
const users = await api.get('/users', { query: { role: 'admin' } });
const user = await api.post('/users', { name: 'John', age: 30, role: 'user' });
Conclusion
These advanced TypeScript patterns can significantly improve your code quality and developer experience. Remember to:
- Start with simpler patterns and gradually introduce complexity
- Use TypeScript’s type system to prevent bugs at compile time
- Leverage generic constraints to create reusable components
- Document complex type patterns for team understanding
The patterns shown here are just the beginning — TypeScript’s type system is incredibly powerful and can be used to create even more sophisticated patterns for your specific needs.
Do You want to learn more like above content ?
Follow me or message me on Linkedin.