Advanced patterns and techniques for leveraging TypeScript's powerful type system effectively.
Use generics with constraints to create flexible, reusable components while maintaining type safety.
// ✅ Generic constraints for better type safety
interface Identifiable {
id: string;
}
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
// Constrain generic to have specific properties
function updateEntity<T extends Identifiable>(
entity: T,
updates: Partial<Omit<T, 'id'>>
): T {
return { ...entity, ...updates };
}
// Multiple constraints
function createAuditLog<T extends Identifiable & Timestamped>(
entity: T,
action: string
): AuditLog {
return {
entityId: entity.id,
action,
timestamp: new Date(),
entityType: entity.constructor.name
};
}
// ✅ Conditional types for advanced type manipulation
type ApiResponse<T> = T extends string
? { message: T }
: T extends number
? { count: T }
: { data: T };
// Usage examples
type StringResponse = ApiResponse<string>; // { message: string }
type NumberResponse = ApiResponse<number>; // { count: number }
type UserResponse = ApiResponse<User>; // { data: User }
// ✅ Mapped types with conditions
type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type MakeRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
type UserWithOptionalEmail = MakeOptional<User, 'email'>; // email becomes optional
type UserWithRequiredAvatar = MakeRequired<User, 'avatar'>; // avatar becomes required
// ✅ Utility type for deep readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
const config: DeepReadonly<AppConfig> = {
api: {
baseUrl: 'https://api.example.com',
timeout: 5000
}
}; // All nested properties are readonly Leverage TypeScript's built-in utility types and create custom ones for common patterns.
// ✅ Built-in utility types
interface User {
id: string;
name: string;
email: string;
password: string;
role: 'admin' | 'user';
createdAt: Date;
updatedAt: Date;
}
// Pick specific properties
type UserPublic = Pick<User, 'id' | 'name' | 'email' | 'role'>;
// Omit sensitive properties
type UserSafe = Omit<User, 'password'>;
// Make properties optional
type UserUpdate = Partial<Pick<User, 'name' | 'email' | 'role'>>;
// Create user input type
type CreateUserInput = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
// ✅ Custom utility types
type NonEmptyArray<T> = [T, ...T[]];
// Extract function return type
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
async function fetchUser(id: string): Promise<User> {
// implementation
return {} as User;
}
type FetchUserResult = ReturnTypeOf<typeof fetchUser>; // Promise<User>
// ✅ Builder pattern with fluent interface
class QueryBuilder<T> {
private conditions: string[] = [];
private selectFields: string[] = [];
private orderByField?: string;
select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>> {
this.selectFields = fields as string[];
return this as any;
}
where(condition: string): QueryBuilder<T> {
this.conditions.push(condition);
return this;
}
orderBy<K extends keyof T>(field: K): QueryBuilder<T> {
this.orderByField = field as string;
return this;
}
build(): string {
let query = `SELECT ${this.selectFields.join(', ')} FROM table`;
if (this.conditions.length > 0) {
query += ` WHERE ${this.conditions.join(' AND ')}`;
}
if (this.orderByField) {
query += ` ORDER BY ${this.orderByField}`;
}
return query;
}
}
// Usage with type safety
const query = new QueryBuilder<User>()
.select('id', 'name', 'email') // TypeScript knows these are valid User keys
.where('active = true')
.orderBy('createdAt')
.build(); Implement robust error handling patterns with proper TypeScript typing.
// ✅ Result pattern for error handling
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUserSafe(id: string): Promise<Result<User, string>> {
try {
const user = await fetchUser(id);
return { success: true, data: user };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
// Usage with type safety
async function handleUser(id: string): Promise<void> {
const result = await fetchUserSafe(id);
if (result.success) {
console.log(result.data.name); // TypeScript knows data is User
} else {
console.error(result.error); // TypeScript knows error is string
}
}
// ✅ Custom error classes with proper typing
abstract class AppError extends Error {
abstract readonly statusCode: number;
abstract readonly isOperational: boolean;
}
class ValidationError extends AppError {
readonly statusCode = 400;
readonly isOperational = true;
constructor(
public readonly field: string,
message: string
) {
super(message);
this.name = 'ValidationError';
}
}
class NotFoundError extends AppError {
readonly statusCode = 404;
readonly isOperational = true;
constructor(resource: string, id: string) {
super(`${resource} with ID ${id} not found`);
this.name = 'NotFoundError';
}
}
// ✅ Runtime validation with type guards
function validateUser(data: unknown): User {
if (!isObject(data)) {
throw new ValidationError('user', 'User data must be an object');
}
if (!isString(data.name) || data.name.trim().length === 0) {
throw new ValidationError('name', 'Name is required and must be a non-empty string');
}
if (!isString(data.email) || !isValidEmail(data.email)) {
throw new ValidationError('email', 'Valid email is required');
}
return data as User; // Safe cast after validation
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isValidEmail(email: string): boolean {
return /^[^s@]+@[^s@]+.[^s@]+$/.test(email);
}
// ✅ Async error handling with proper types
type AsyncResult<T> = Promise<Result<T, AppError>>;
class UserService {
async createUser(userData: CreateUserInput): AsyncResult<User> {
try {
const validatedData = validateUser(userData);
const user = await this.saveUser(validatedData);
return { success: true, data: user };
} catch (error) {
if (error instanceof AppError) {
return { success: false, error };
}
return {
success: false,
error: new ValidationError('general', 'Failed to create user')
};
}
}
private async saveUser(userData: CreateUserInput): Promise<User> {
// Database save implementation
return {} as User;
}
} Implement sophisticated function patterns with proper type safety and flexibility.
// ✅ Function overloads for flexible APIs
function createElement(tag: 'img'): HTMLImageElement;
function createElement(tag: 'input'): HTMLInputElement;
function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}
// Usage - TypeScript knows the specific return type
const img = createElement('img'); // HTMLImageElement
const input = createElement('input'); // HTMLInputElement
const div = createElement('div'); // HTMLDivElement
// ✅ Higher-order functions with proper typing
type EventHandler<T> = (event: T) => void;
type UnsubscribeFunction = () => void;
function createEventEmitter<TEvents extends Record<string, any>>() {
const listeners: { [K in keyof TEvents]?: EventHandler<TEvents[K]>[] } = {};
return {
on<K extends keyof TEvents>(
event: K,
handler: EventHandler<TEvents[K]>
): UnsubscribeFunction {
if (!listeners[event]) {
listeners[event] = [];
}
listeners[event]!.push(handler);
return () => {
const handlers = listeners[event];
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
};
},
emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void {
const handlers = listeners[event];
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
};
}
// Usage with type safety
interface AppEvents {
userLogin: { userId: string; timestamp: Date };
userLogout: { userId: string };
dataUpdate: { table: string; id: string; changes: any };
}
const eventEmitter = createEventEmitter<AppEvents>();
// TypeScript knows the event data type
eventEmitter.on('userLogin', (data) => {
console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});
// ✅ Decorator pattern with proper typing
function memoize<T extends (...args: any[]) => any>(
target: T
): T {
const cache = new Map();
return ((...args: Parameters<T>) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = target(...args);
cache.set(key, result);
return result;
}) as T;
}
// Usage
const expensiveCalculation = memoize((x: number, y: number): number => {
console.log('Calculating...');
return x * y + Math.random();
});
// ✅ Currying with proper type inference
function curry<A, B, C>(
fn: (a: A, b: B) => C
): (a: A) => (b: B) => C;
function curry<A, B, C, D>(
fn: (a: A, b: B, c: C) => D
): (a: A) => (b: B) => (c: C) => D;
function curry(fn: Function): Function {
return function curried(...args: any[]): any {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function (...args2: any[]) {
return curried.apply(this, args.concat(args2));
};
}
};
}
// Usage with type safety
const add = (a: number, b: number) => a + b;
const curriedAdd = curry(add);
const addTen = curriedAdd(10); // (b: number) => number
const result = addTen(5); // 15 Optimize TypeScript compilation and runtime performance with these techniques.
// ✅ Use const assertions for immutable data
const SUPPORTED_FORMATS = ['jpg', 'png', 'gif', 'webp'] as const;
type SupportedFormat = typeof SUPPORTED_FORMATS[number]; // 'jpg' | 'png' | 'gif' | 'webp'
const CONFIG = {
api: {
baseUrl: 'https://api.example.com',
timeout: 5000
},
features: {
darkMode: true,
notifications: false
}
} as const;
// ✅ Prefer union types over enums for better tree-shaking
type Status = 'loading' | 'success' | 'error' | 'idle';
const STATUS = {
LOADING: 'loading' as const,
SUCCESS: 'success' as const,
ERROR: 'error' as const,
IDLE: 'idle' as const
} as const;
// ✅ Use type-only imports to reduce bundle size
import type { User, UserRole } from './types/user';
import type { ApiResponse } from './types/api';
// Regular import for runtime values
import { validateUser, createUser } from './services/user';
// ✅ Lazy type evaluation for large types
type LazyUser<T = never> = T extends never
? {
id: string;
profile: {
name: string;
email: string;
preferences: {
theme: 'light' | 'dark';
language: string;
notifications: boolean;
};
};
}
: T;
// Only evaluated when used
type ConcreteUser = LazyUser<true>;
// ✅ Efficient type guards
function isUserArray(value: unknown): value is User[] {
return Array.isArray(value) && value.length > 0 && isUser(value[0]);
}
// ✅ Branded types for nominal typing
type UserId = string & { readonly brand: unique symbol };
type PostId = string & { readonly brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}
function createPostId(id: string): PostId {
return id as PostId;
}
// These are now different types even though they're both strings
function getUser(id: UserId): Promise<User> {
// Implementation
return Promise.resolve({} as User);
}
function getPost(id: PostId): Promise<Post> {
// Implementation
return Promise.resolve({} as Post);
}
// ❌ This would be a TypeScript error
// getUser(createPostId('123')); // Error: PostId is not assignable to UserId
// ✅ Template literal types for compile-time validation
type EventName = `user:${string}` | `post:${string}` | `system:${string}`;
function emitEvent(name: EventName, data: any): void {
// Implementation
}
// Valid
emitEvent('user:login', { userId: '123' });
emitEvent('post:created', { postId: '456' });
// ❌ TypeScript error
// emitEvent('invalid:event', {}); // Error: not assignable to EventName import type for
type-only imports