Skip to main content

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!