Introduction
TypeScript’s type system is remarkably powerful. Beyond basic types, advanced features enable sophisticated patterns for type-safe APIs, runtime validation, and code generation. This guide explores advanced TypeScript patterns that will elevate your code.
Generics Deep Dive
Generic Constraints
// Basic generic
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// Constraining to a type
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}
// Now works with string, array, but not number
logLength("hello"); // OK
logLength([1, 2, 3]); // OK
logLength({ length: 5 }); // OK
// logLength(123); // Error: number has no length
Generic Classes
interface Repository<T> {
find(id: string): Promise<T | null>;
save(entity: T): Promise<T>;
delete(id: string): Promise<void>;
}
class UserRepository implements Repository<User> {
private users: Map<string, User> = new Map();
async find(id: string): Promise<User | null> {
return this.users.get(id) ?? null;
}
async save(user: User): Promise<User> {
this.users.set(user.id, user);
return user;
}
async delete(id: string): Promise<void> {
this.users.delete(id);
}
}
Generic Factories
type Constructor<T> = new (...args: any[]) => T;
function createService<T>(
base: Constructor<T>,
apiClient: ApiClient
): T & { api: ApiClient } {
const instance = new base();
return Object.assign(instance, { api: apiClient });
}
class UserService {
async getUser(id: string) { /* ... */ }
}
const service = createService(UserService, new ApiClient());
service.getUser('123'); // From UserService
service.api.get('/users'); // From apiClient
Conditional Types
Basic Conditionals
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
Extract and Exclude
type T = string | number | boolean;
// Extract strings
type Strings = Extract<T, string>; // string
// Exclude strings
type NonStrings = Exclude<T, string>; // number | boolean
Practical Example: API Response Types
type APISuccess<T> = {
success: true;
data: T;
};
type APIError = {
success: false;
error: {
code: string;
message: string;
};
};
type APIResponse<T> = APISuccess<T> | APIError;
// Type-safe response handler
function handleResponse<T>(response: APIResponse<T>) {
if (response.success) {
// TypeScript knows response.data exists here
return response.data;
}
// TypeScript knows error exists here
throw new Error(response.error.message);
}
Template Literal Types
Basic Usage
type EventName = 'click' | 'focus' | 'blur';
type Handler = `on${Capitalize<EventName>}`;
// Handler = "onClick" | "onFocus" | "onBlur"
type HTTPMethod = 'get' | 'post' | 'put' | 'delete';
type Endpoint = `/${string}`;
type Route = `${Uppercase<HTTPMethod>} ${Endpoint}`;
// Route = "GET /users" | "POST /users" | ...
Building Type-Safe APIs
type PathParams<T extends string> =
T extends `${string}:${infer P}/${infer Rest}`
? P | PathParams<`/${Rest}`>
: T extends `${string}:${infer P}`
? P
: never;
type Route = '/users/:id/posts/:postId';
type Params = PathParams<Route>; // "id" | "postId"
// Extract params into an object
type RouteParams<T extends string> =
T extends `${string}:${infer}`
? { [K in P P}/${infer Rest]: string } & RouteParams<`/${Rest}`>
: T extends `${string}:${infer P}`
? { [K in P]: string }
: {};
type Params = RouteParams<Route>;
// { id: string; postId: string }
Mapped Types
Basic Mapping
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Partial<T> = {
[K in keyof T]?: T[K];
};
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
Practical Mapped Types
// Make all properties optional recursively
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
type User = {
name: string;
address: {
street: string;
city: string;
};
};
type PartialUser = DeepPartial<User>;
// { name?: string; address?: { street?: string; city?: string } }
// Make all properties required
type Required<T> = {
[K in keyof T]-?: T[K];
};
Utility Types
Custom Utility Types
// Omit multiple keys
type OmitKeys<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P];
};
// Pick optional keys
type PickOptional<T> = {
[P in keyof T as T[P] extends undefined ? never : P]: T[P];
};
// Make specific keys nullable
type Nullable<T, K extends keyof T> = Omit<T, K> & {
[P in K]: T[P] | null;
};
// Example usage
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
type CreateUser = OmitKeys<User, 'id' | 'createdAt'>;
type UserWithEmail = Nullable<User, 'email'>;
Type Guards
Custom Type Guards
interface Fish {
swim(): void;
}
interface Bird {
fly(): void;
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // TypeScript knows it's Fish
} else {
pet.fly(); // TypeScript knows it's Bird
}
}
Discriminated Unions
type Action =
| { type: 'increment'; amount: number }
| { type: 'decrement'; amount: number }
| { type: 'reset' };
function reducer(action: Action) {
switch (action.type) {
case 'increment':
return action.amount; // TypeScript knows amount exists
case 'decrement':
return action.amount;
case 'reset':
return 0;
}
}
Declaration Merging
Interface Merging
interface User {
name: string;
}
interface User {
age: number;
}
// Merged: User { name: string; age: number; }
Namespace Merging
namespace Validation {
export function isEmail(value: string): boolean {
return value.includes('@');
}
}
namespace Validation {
export function isUrl(value: string): boolean {
return value.startsWith('http');
}
}
// Now Validation has both functions
Decorators (Experimental)
Class Decorators
function singleton<T extends Constructor>(constructor: T) {
let instance: InstanceType<T> | null = null;
return class extends constructor {
constructor(...args: any[]) {
if (!instance) {
super(...args);
instance = this as InstanceType<T>;
}
return instance;
}
};
}
@singleton
class Database {
constructor() {}
}
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true
Method Decorators
function logged<T>(
target: any,
propertyKey: string,
descriptor: TypedPropertyDescriptor<T>
) {
const original = descriptor.value!;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with:`, args);
const result = original.apply(this, args);
console.log(`Result:`, result);
return result;
} as T;
return descriptor;
}
class Calculator {
@logged
add(a: number, b: number): number {
return a + b;
}
}
Type-Safe Event System
type EventMap = {
click: { x: number; y: number };
keypress: { key: string };
focus: void;
};
class EventEmitter<T extends Record<string, any>> {
private listeners: {
[K in keyof T]?: Array<(payload: T[K]) => void>;
} = {};
on<K extends keyof T>(event: K, callback: (payload: T[K]) => void) {
this.listeners[event]?.push(callback);
}
emit<K extends keyof T>(event: K, payload: T[K]) {
this.listeners[event]?.forEach(cb => cb(payload));
}
}
const emitter = new EventEmitter<EventMap>();
emitter.on('click', ({ x, y }) => console.log(x, y));
emitter.emit('click', { x: 10, y: 20 });
Conclusion
TypeScript’s advanced types enable powerful patterns for building type-safe applications. From generics to conditional types, mapped types to decorators, these tools help catch errors at compile time and create self-documenting code. Master these patterns to level up your TypeScript skills.
Comments