TypeScript Advanced Patterns - Mastering Type-Safe Development
· 9 min read
TypeScript's advanced type system enables powerful patterns that can make your code more robust, maintainable, and type-safe. This guide explores advanced TypeScript patterns that every developer should master.
Advanced Generic Patterns
Generic Constraints and Conditional Types
// Generic with constraints
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
// Conditional types
type NonNullable<T> = T extends null | undefined ? never : T;
type ApiResponse<T> = T extends string
? { message: T }
: T extends number
? { count: T }
: { data: T };
// Usage
type StringResponse = ApiResponse<string>; // { message: string }
type NumberResponse = ApiResponse<number>; // { count: number }
type ObjectResponse = ApiResponse<{ id: number }>; // { data: { id: number } }
Mapped Types and Key Manipulation
// Basic mapped type
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Advanced mapped type with key manipulation
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
// Example usage
interface User {
id: number;
name: string;
email: string;
}
type UserGetters = Getters<User>;
// {
// getId: () => number;
// getName: () => string;
// getEmail: () => string;
// }
type UserSetters = Setters<User>;
// {
// setId: (value: number) => void;
// setName: (value: string) => void;
// setEmail: (value: string) => void;
// }
Template Literal Types
// Basic template literal types
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type HoverEvent = EventName<'hover'>; // 'onHover'
// Advanced template literal patterns
type ApiEndpoint<T extends string> = `/api/${T}`;
type UserEndpoint = ApiEndpoint<'users'>; // '/api/users'
// CSS property names
type CSSProperty<T extends string> = `--${T}`;
type CustomProperty = CSSProperty<'primary-color'>; // '--primary-color'
// Database table naming
type TableName<T extends string> = `${T}_table`;
type UserTable = TableName<'user'>; // 'user_table'
Advanced Utility Types
Custom Utility Types
// Deep partial - makes all nested properties optional
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Required by keys
type RequiredBy<T, K extends keyof T> = T & Required<Pick<T, K>>;
// Optional by keys
type OptionalBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Function parameters
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
// Return type
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
// Example usage
interface User {
id: number;
name: string;
email: string;
profile?: {
avatar?: string;
bio?: string;
};
}
type UserWithRequiredEmail = RequiredBy<User, 'email'>;
type UserWithOptionalProfile = OptionalBy<User, 'profile'>;
type DeepPartialUser = DeepPartial<User>;
Conditional Type Utilities
// Extract specific types
type ExtractArrayType<T> = T extends (infer U)[] ? U : never;
type StringArray = ExtractArrayType<string[]>; // string
// Extract function return type
type ExtractReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Check if type is array
type IsArray<T> = T extends any[] ? true : false;
type Test1 = IsArray<string[]>; // true
type Test2 = IsArray<string>; // false
// Flatten array types
type Flatten<T> = T extends (infer U)[] ? U : T;
type Flattened = Flatten<string[][]>; // string[]
// Extract promise type
type Awaited<T> = T extends Promise<infer U> ? U : T;
type PromiseString = Awaited<Promise<string>>; // string
Advanced Function Patterns
Function Overloading with Generics
// Function overloads
function process<T extends string>(input: T): `processed_${T}`;
function process<T extends number>(input: T): T;
function process<T extends boolean>(input: T): T;
function process<T>(input: T): T {
if (typeof input === 'string') {
return `processed_${input}` as any;
}
return input;
}
// Advanced function type manipulation
type AsyncFunction<T extends (...args: any[]) => any> =
(...args: Parameters<T>) => Promise<ReturnType<T>>;
type SyncFunction<T extends (...args: any[]) => Promise<any>> =
(...args: Parameters<T>) => ReturnType<T>;
// Convert async function to sync
function makeSync<T extends (...args: any[]) => Promise<any>>(
fn: T
): SyncFunction<T> {
return ((...args: Parameters<T>) => {
// This would need actual implementation
throw new Error('Not implemented');
}) as SyncFunction<T>;
}
Higher-Order Function Types
// Function composition
type Compose<F extends Function, G extends Function> =
F extends (x: infer X) => infer Y
? G extends (x: Y) => infer Z
? (x: X) => Z
: never
: never;
// Curried function types
type Curried<T extends (...args: any[]) => any> =
T extends (first: infer First, ...rest: infer Rest) => infer Return
? Rest extends []
? T
: (first: First) => Curried<(...args: Rest) => Return>
: never;
// Example
function add(a: number, b: number): number {
return a + b;
}
type CurriedAdd = Curried<typeof add>; // (a: number) => (b: number) => number
Advanced Object Patterns
Branded Types
// Branded types for type safety
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<number, 'UserId'>;
type ProductId = Brand<number, 'ProductId'>;
function createUserId(id: number): UserId {
return id as UserId;
}
function createProductId(id: number): ProductId {
return id as ProductId;
}
// This prevents mixing up different ID types
function getUser(id: UserId) {
// Implementation
}
const userId = createUserId(123);
const productId = createProductId(456);
getUser(userId); // OK
getUser(productId); // Error: Argument of type 'ProductId' is not assignable to parameter of type 'UserId'
Discriminated Unions
// Discriminated union pattern
type LoadingState = {
status: 'loading';
};
type SuccessState<T> = {
status: 'success';
data: T;
};
type ErrorState = {
status: 'error';
error: string;
};
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
// Type guards
function isLoading<T>(state: AsyncState<T>): state is LoadingState {
return state.status === 'loading';
}
function isSuccess<T>(state: AsyncState<T>): state is SuccessState<T> {
return state.status === 'success';
}
function isError<T>(state: AsyncState<T>): state is ErrorState {
return state.status === 'error';
}
// Usage
function handleState<T>(state: AsyncState<T>) {
if (isLoading(state)) {
console.log('Loading...');
} else if (isSuccess(state)) {
console.log('Data:', state.data);
} else if (isError(state)) {
console.log('Error:', state.error);
}
}
Advanced Class Patterns
Mixins with Generics
// Mixin pattern
type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
timestamp = Date.now();
};
}
function Tagged<TBase extends Constructor>(Base: TBase) {
return class extends Base {
tags: string[] = [];
addTag(tag: string) {
this.tags.push(tag);
}
};
}
// Usage
class User {
constructor(public name: string) {}
}
const TimestampedUser = Timestamped(User);
const TaggedUser = Tagged(User);
const TimestampedAndTaggedUser = Timestamped(Tagged(User));
const user = new TimestampedAndTaggedUser('John');
console.log(user.timestamp);
user.addTag('admin');
Abstract Factory Pattern
// Abstract factory with generics
interface Product {
name: string;
price: number;
}
interface ProductFactory<T extends Product> {
createProduct(name: string, price: number): T;
}
class Book implements Product {
constructor(public name: string, public price: number, public author: string) {}
}
class BookFactory implements ProductFactory<Book> {
createProduct(name: string, price: number): Book {
return new Book(name, price, 'Unknown Author');
}
}
// Generic factory registry
class FactoryRegistry {
private factories = new Map<string, ProductFactory<any>>();
register<T extends Product>(type: string, factory: ProductFactory<T>) {
this.factories.set(type, factory);
}
create<T extends Product>(type: string, name: string, price: number): T {
const factory = this.factories.get(type);
if (!factory) {
throw new Error(`Factory for type ${type} not found`);
}
return factory.createProduct(name, price) as T;
}
}
Advanced Module Patterns
Module Augmentation
// Augment existing modules
declare global {
interface Array<T> {
groupBy<K extends string | number | symbol>(
keyFn: (item: T) => K
): Record<K, T[]>;
}
}
// Implementation
Array.prototype.groupBy = function<K extends string | number | symbol>(
this: any[],
keyFn: (item: any) => K
): Record<K, any[]> {
return this.reduce((groups, item) => {
const key = keyFn(item);
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
return groups;
}, {} as Record<K, any[]>);
};
// Usage
const users = [
{ name: 'John', age: 25 },
{ name: 'Jane', age: 30 },
{ name: 'Bob', age: 25 }
];
const groupedByAge = users.groupBy(user => user.age);
// { 25: [{ name: 'John', age: 25 }, { name: 'Bob', age: 25 }], 30: [{ name: 'Jane', age: 30 }] }
Conditional Module Loading
// Conditional module loading types
type ModuleLoader<T extends string> = T extends 'development'
? typeof import('./dev-module')
: T extends 'production'
? typeof import('./prod-module')
: never;
// Environment-based module loading
async function loadModule<T extends 'development' | 'production'>(
env: T
): Promise<ModuleLoader<T>> {
if (env === 'development') {
return import('./dev-module');
} else {
return import('./prod-module');
}
}
Advanced Error Handling
Result Type Pattern
// Result type for error handling
type Result<T, E = Error> = Success<T> | Failure<E>;
type Success<T> = {
success: true;
data: T;
};
type Failure<E> = {
success: false;
error: E;
};
// Result utilities
function success<T>(data: T): Success<T> {
return { success: true, data };
}
function failure<E>(error: E): Failure<E> {
return { success: false, error };
}
// Async result type
type AsyncResult<T, E = Error> = Promise<Result<T, E>>;
// Example usage
async function fetchUser(id: number): AsyncResult<User, string> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return failure(`HTTP ${response.status}: ${response.statusText}`);
}
const user = await response.json();
return success(user);
} catch (error) {
return failure(error instanceof Error ? error.message : 'Unknown error');
}
}
// Result chaining
function mapResult<T, U, E>(
result: Result<T, E>,
fn: (data: T) => U
): Result<U, E> {
if (result.success) {
return success(fn(result.data));
}
return result;
}
Performance and Optimization
Type-Level Optimization
// Lazy evaluation types
type Lazy<T> = () => T;
function lazy<T>(fn: () => T): Lazy<T> {
let cached: T | undefined;
return () => {
if (cached === undefined) {
cached = fn();
}
return cached;
};
}
// Memoization types
type MemoizedFunction<T extends (...args: any[]) => any> = T & {
cache: Map<string, ReturnType<T>>;
clearCache(): void;
};
function memoize<T extends (...args: any[]) => any>(fn: T): MemoizedFunction<T> {
const cache = new Map<string, ReturnType<T>>();
const memoized = ((...args: Parameters<T>) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key)!;
}
const result = fn(...args);
cache.set(key, result);
return result;
}) as MemoizedFunction<T>;
memoized.cache = cache;
memoized.clearCache = () => cache.clear();
return memoized;
}
Testing with Advanced Types
Type Testing Utilities
// Type testing utilities
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false;
// Type tests
type Test1 = Expect<Equal<string, string>>; // true
type Test2 = Expect<Equal<string, number>>; // Error: Type 'false' is not assignable to type 'true'
// Assert type equality
type AssertEqual<T, U> = T extends U ? (U extends T ? true : false) : false;
// Test utility types
type TestPartial = AssertEqual<Partial<{ a: number; b: string }>, { a?: number; b?: string }>; // true
type TestRequired = AssertEqual<Required<{ a?: number; b?: string }>, { a: number; b: string }>; // true
Conclusion
Advanced TypeScript patterns provide powerful tools for creating robust, type-safe applications. By mastering these patterns, you can:
- Create more maintainable and self-documenting code
- Catch errors at compile time rather than runtime
- Build flexible and reusable type systems
- Improve developer experience with better IDE support
Next Steps
Ready to dive deeper into TypeScript? Check out our comprehensive tutorials:
What advanced TypeScript patterns do you find most useful? Share your experiences and tips in the comments below!