TypeScript Advanced Type Patterns Every Senior Developer Should Know
Master TypeScript's type system with advanced patterns including conditional types, mapped types, template literals, and real-world architectural patterns.
Introduction
TypeScript’s type system is remarkably powerful. Beyond basic types, it offers advanced features that enable sophisticated type-level programming. This article explores patterns I use daily as a senior engineer to build bulletproof APIs and maintainable codebases.
1. Discriminated Unions & Type Guards
The foundation of type-safe state management:
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function handleState<T>(state: RequestState<T>) {
switch (state.status) {
case 'idle':
return 'Waiting to start...';
case 'loading':
return 'Fetching data...';
case 'success':
return `Got: ${state.data}`; // TypeScript knows data exists
case 'error':
return `Error: ${state.error.message}`; // TypeScript knows error exists
}
}
Key Insight: Discriminated unions let TypeScript narrow types automatically within switch/if statements.
2. Template Literal Types
Build type-safe APIs with string manipulation:
// API Response Types
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/posts' | '/comments';
type APIRoute = `${HTTPMethod} ${Endpoint}`;
// Event System
type EventName = 'click' | 'focus' | 'blur';
type Handler = `on${Capitalize<EventName>}`;
type EventHandlers = {
[K in Handler]: () => void;
};
// Results in: { onClick: () => void; onFocus: () => void; onBlur: () => void; }
3. Mapped Types with Modifiers
Transform types systematically:
// Make all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Make all properties readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Remove specific properties
type Omit<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P];
};
// Real-world: Make API response nullable
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
type UserResponse = {
id: string;
name: string;
email: string;
};
type NullableUser = Nullable<UserResponse>;
// { id: string | null; name: string | null; email: string | null; }
4. Conditional Types
Type-level programming with inference:
// Extract return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Extract parameter types
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
// Flatten nested arrays
type Flatten<T> = T extends (infer U)[] ? U : T;
type Nested = string[][];
type Flat = Flatten<Nested>; // string[]
// Real-world: Extract array element type
type ArrayElement<T> = T extends readonly (infer U)[] ? U : never;
// Practical: Get element from tuple
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type Tuple = [string, number, boolean];
type FirstEl = First<Tuple>; // string
type LastEl = Last<Tuple>; // boolean
5. The Builder Pattern with TypeScript
Type-safe fluent interfaces:
class QueryBuilder<T extends string = ''> {
private query: string = T;
select<K extends string>(fields: K[]): QueryBuilder<`SELECT ${K} FROM ${T}`> {
return this as any;
}
from<K extends string>(table: K): QueryBuilder<`SELECT * FROM ${K}`> {
return this as any;
}
where(condition: string): this {
this.query += ` WHERE ${condition}`;
return this;
}
build(): string {
return this.query;
}
}
// Usage with full type safety
const query = new QueryBuilder()
.from('users')
.where('age > 18')
.build();
// TypeScript tracks the query structure!
6..brand() Pattern for Type Branding
Create nominal types from primitives:
type Brand<T, B> = T & { __brand: B };
type UserID = Brand<string, 'UserID'>;
type PostID = Brand<string, 'PostID'>;
function getUserById(id: UserID): User { /* ... */ }
function getPostById(id: PostID): Post { /* ... */ }
const userId = 'abc123' as UserID;
const postId = 'xyz789' as PostID;
getUserById(userId); // ✅ Works
getUserById(postId); // ❌ TypeScript error! Can't mix UserID and PostID
7. Deep Partial & Required
Recursive type transformations:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};
type Config = {
server: {
port: number;
host: string;
ssl: {
enabled: boolean;
cert: string;
};
};
database: {
url: string;
};
};
type PartialConfig = DeepPartial<Config>;
// All nested properties become optional
type RequiredConfig = DeepRequired<PartialConfig>;
// All properties become required again
8. Type-Safe Event Emitter
Practical real-world pattern:
type EventMap = {
user:login: { userId: string; timestamp: Date };
user:logout: { userId: string };
data:sync: { records: number };
error: [Error, string?]; // Tuple for multiple args
};
type EventKeys = keyof EventMap;
type EventReceiver<K extends EventKeys> = EventMap[K];
class Emitter<Events extends Record<string, any>> {
private listeners: {
[K in keyof Events]?: Array<(data: Events[K]) => void>;
} = {};
on<K extends keyof Events>(event: K, fn: (data: Events[K]) => void): void {
(this.listeners[event] ??= []).push(fn);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
(this.listeners[event] ??= []).forEach(fn => fn(data));
}
}
const emitter = new Emitter<EventMap>();
emitter.on('user:login', ({ userId, timestamp }) => {
console.log(`${userId} logged in at ${timestamp}`);
});
emitter.emit('user:login', {
userId: '123',
timestamp: new Date()
});
Conclusion
These patterns form the backbone of TypeScript’s most powerful features. Master them, and you’ll:
- Catch bugs at compile time rather than runtime
- Create self-documenting APIs that are impossible to misuse
- Build maintainable systems that scale with your team
The key is understanding that TypeScript’s type system is a full programming language at the type level. Think in types, and the compiler will work for you.
Next in this series: I’ll explore advanced Rust patterns for systems programming.