Explore sophisticated TypeScript patterns that will level up your codebase. From conditional types to template literals, discover the advanced features that make TypeScript truly powerful.
Advanced TypeScript Patterns: Beyond the Basics
You've mastered the basics of TypeScript. You know your interfaces from your types, you're comfortable with generics, and you can spot a any usage from a mile away. But are you ready to level up?
Today we're diving deep into the advanced patterns that separate good TypeScript developers from great ones.
🎯 Conditional Types: The Ultimate Decision Maker
Conditional types are like having a built-in if-statement for your type system. They allow you to create types that change based on conditions.
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
// Real-world example: API response handling
type ApiResponse<T> = T extends 'success'
? { status: 'success'; data: unknown }
: { status: 'error'; error: string };
type SuccessResponse = ApiResponse<'success'>;
// ^? { status: 'success'; data: unknown }
type ErrorResponse = ApiResponse<'error'>;
// ^? { status: 'error'; error: string }
🔥 Template Literal Types: String Manipulation at the Type Level
TypeScript 4.1 brought us the ability to manipulate strings right in the type system. This might sound like a party trick, but it's incredibly powerful for creating type-safe APIs.
// Create event handler types automatically
type EventHandler = `on${Capitalize<string>}`;
type ButtonEvents = {
onClick: (event: MouseEvent) => void;
onHover: (event: MouseEvent) => void;
onFocus: (event: FocusEvent) => void;
};
// Extract event names
type EventNames = keyof ButtonEvents;
// ^? "onClick" | "onHover" | "onFocus"
// Create CSS-in-JS property types
type CssProperty = `margin${'Top' | 'Bottom' | 'Left' | 'Right'}`
| `padding${'Top' | 'Bottom' | 'Left' | 'Right'}`;
const styles: Record<CssProperty, string> = {
marginTop: '10px',
marginBottom: '20px',
marginLeft: '5px',
marginRight: '5px',
paddingTop: '15px',
paddingBottom: '15px',
paddingLeft: '10px',
paddingRight: '10px',
};
🧙♂️ Mapped Types with Constraints: Dynamic Type Creation
Combine mapped types with constraints to create powerful, flexible type utilities.
// Create a type that makes all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Create a type that makes all properties required
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Advanced: Pick properties that match a certain type
type PropertiesOfType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K]
};
interface User {
id: number;
name: string;
email: string;
age: number;
isActive: boolean;
}
type StringProperties = PropertiesOfType<User, string>;
// ^? { name: string; email: string }
type NumberProperties = PropertiesOfType<User, number>;
// ^? { id: number; age: number }
🎨 Utility Types on Steroids
Go beyond the built-in utility types and create your own powerful type transformations.
// Deep partial - makes all nested properties optional
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
api: {
timeout: number;
retries: number;
};
}
type PartialConfig = DeepPartial<Config>;
// Now you can partially update nested objects!
// Flatten nested object types
type Flatten<T> = {
[K in keyof T]: T[K] extends object
? T[K] extends infer O
? O extends object
? { [P in keyof O]: O[P] }
: T[K]
: T[K]
: T[K]
};
// Type-safe function parameters
type SafeParameters<T extends (...args: any) => any> = Parameters<T> extends infer P
? P extends readonly any[]
? { [K in keyof P]: P[K] }
: never
: never;
🚀 Brand Types: Make Your Types Truly Unique
Brand types prevent accidental assignment between types that are structurally the same but semantically different.
// Create branded types
type Branded<T, B> = T & { __brand: B };
type UserId = Branded<string, 'UserId'>;
type EmailAddress = Branded<string, 'EmailAddress'>;
// Helper functions to create branded values
function createUserId(id: string): UserId {
return id as UserId;
}
function createEmail(email: string): EmailAddress {
if (!email.includes('@')) {
throw new Error('Invalid email format');
}
return email as EmailAddress;
}
// Now these can't be accidentally mixed up!
function processUser(id: UserId, email: EmailAddress) {
console.log(`Processing user ${id} with email ${email}`);
}
const userId = createUserId('user-123');
const email = createEmail('user@example.com');
processUser(userId, email); // ✅ Works
// processUser(email, userId); // ❌ TypeScript error!
🎯 Real-World Example: Type-Safe Event System
Let's put it all together with a practical example of a type-safe event system.
// Define event types
type EventType = 'user:created' | 'user:updated' | 'post:published';
type EventPayload<T extends EventType> = T extends 'user:created'
? { id: string; name: string; email: string }
: T extends 'user:updated'
? { id: string; changes: Partial<{ name: string; email: string }> }
: T extends 'post:published'
? { id: string; title: string; authorId: string }
: never;
// Type-safe event emitter
class EventEmitter<T extends EventType> {
private listeners = new Map<T, Set<(payload: EventPayload<T>) => void>>();
on<K extends T>(event: K, listener: (payload: EventPayload<K>) => void): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
}
emit<K extends T>(event: K, payload: EventPayload<K>): void {
const eventListeners = this.listeners.get(event);
if (eventListeners) {
eventListeners.forEach(listener => listener(payload));
}
}
}
// Usage
const emitter = new EventEmitter<EventType>();
emitter.on('user:created', (payload) => {
// payload is automatically typed as { id: string; name: string; email: string }
console.log(`User created: ${payload.name}`);
});
emitter.emit('user:created', {
id: '123',
name: 'John Doe',
email: 'john@example.com'
});
🤔 When to Use These Advanced Patterns
These patterns are powerful, but with great power comes great responsibility. Use them when:
- Building libraries or frameworks - Type safety is crucial for public APIs
- Complex domain modeling - When your business logic needs strict type enforcement
- Code generation - When you're generating types from schemas or APIs
- Large codebases - When the overhead pays off in maintainability
Avoid them when:
- You're writing simple scripts or utilities
- Your team is new to TypeScript
- The complexity doesn't provide clear benefits
🎯 The Bottom Line
Advanced TypeScript patterns aren't just about showing off your type-fu. They're about creating more robust, maintainable code that catches errors at compile time instead of runtime.
Start small, experiment with these patterns, and gradually incorporate them into your codebase. Your future self (and your team) will thank you.
What's your favorite advanced TypeScript pattern? Share your most creative type solutions in the comments!
Comments