TypeScript's type system is remarkably powerful, enabling patterns that catch bugs at compile time rather than at runtime. Moving beyond basic types to advanced patterns dramatically improves code quality and developer experience. This guide covers TypeScript patterns every senior developer should master.
Why Advanced TypeScript
Mastering advanced TypeScript enables:
- Compile-Time Safety: Catch errors before they reach production
- Better IntelliSense: IDE knows your code's shape
- Self-Documenting Code: Types describe intent
- Refactoring Confidence: Type checker catches breaking changes
- API Design: Express constraints in the type system
Utility Types
Built-in Utility Types
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Partial: All properties optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; ... }
// Required: All properties required
type CompleteUser = Required<Partial<User>>;
// Pick: Select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string; }
// Omit: Exclude specific properties
type UserWithoutPassword = Omit<User, 'password'>;
// { id: number; name: string; email: string; createdAt: Date; }
// Readonly: All properties readonly
type ImmutableUser = Readonly<User>;
// Cannot reassign properties
// Record: Create object type with keys and value type
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
// { [key: string]: 'admin' | 'user' | 'guest' }
// Extract: Extract types from union
type StringOrNumber = string | number | boolean;
type OnlyStrings = Extract<StringOrNumber, string>;
// string
// Exclude: Remove types from union
type NotString = Exclude<StringOrNumber, string>;
// number | boolean
// NonNullable: Remove null and undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// string
// ReturnType: Get function return type
function createUser() {
return { id: 1, name: 'John' };
}
type NewUser = ReturnType<typeof createUser>;
// { id: number; name: string; }
// Parameters: Get function parameter types
function greet(name: string, age: number) {}
type GreetParams = Parameters<typeof greet>;
// [string, number]
Generics
Basic Generics
// Generic function
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // number
const str = identity('hello'); // string
// Generic interface
interface Container<T> {
value: T;
getValue(): T;
}
// Generic class
class Box<T> {
constructor(private content: T) {}
getContent(): T {
return this.content;
}
}
const numberBox = new Box(123);
const stringBox = new Box('hello');
Constrained Generics
// Constraint with extends
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}
logLength('hello'); // OK
logLength([1, 2, 3]); // OK
logLength({ length: 5 }); // OK
// logLength(123); // Error: number doesn't have length
// Multiple constraints
interface HasId {
id: number;
}
interface HasName {
name: string;
}
function merge<T extends HasId, U extends HasName>(a: T, b: U): T & U {
return { ...a, ...b };
}
// keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'John', age: 30 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age'); // number
// getProperty(user, 'invalid'); // Error
Default Generic Types
interface ApiResponse<T = unknown> {
data: T;
status: number;
message: string;
}
// Uses default
const response: ApiResponse = { data: {}, status: 200, message: 'OK' };
// Explicit type
const userResponse: ApiResponse<User> = {
data: { id: 1, name: 'John', email: '[email protected]', password: '', createdAt: new Date() },
status: 200,
message: 'OK'
};
Conditional Types
Basic Conditional Types
// T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Practical example: unwrap Promise
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type PromiseString = Promise<string>;
type Unwrapped = Unwrap<PromiseString>; // string
type NotPromise = Unwrap<number>; // number
// Array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type StringArrayElement = ArrayElement<string[]>; // string
Distributive Conditional Types
type ToArray<T> = T extends any ? T[] : never;
// Distributes over union
type StringOrNumberArray = ToArray<string | number>;
// string[] | number[]
// Prevent distribution with []
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Combined = ToArrayNonDist<string | number>;
// (string | number)[]
Mapped Types
Basic Mapped Types
// Make all properties optional
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Make all properties required
type Required<T> = {
[K in keyof T]-?: T[K];
};
// Make all properties readonly
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// Remove readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
Advanced Mapped Types
// Transform property types
type Stringify<T> = {
[K in keyof T]: string;
};
// Filter properties by value type
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
}
type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string; }
// Rename properties
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
Template Literal Types
Basic Template Literals
type Greeting = `Hello, ${string}!`;
const valid: Greeting = 'Hello, World!'; // OK
// const invalid: Greeting = 'Hi, World!'; // Error
// Combining unions
type Color = 'red' | 'green' | 'blue';
type Size = 'small' | 'medium' | 'large';
type ColoredSize = `${Color}-${Size}`;
// 'red-small' | 'red-medium' | 'red-large' | 'green-small' | ...
// HTTP methods
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/posts';
type Route = `${Method} ${Endpoint}`;
Event Handler Types
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type ChangeEvent = EventName<'change'>; // 'onChange'
// Create event handler type
type EventHandlers<T> = {
[K in keyof T as EventName<string & K>]: (value: T[K]) => void;
};
interface FormFields {
name: string;
email: string;
age: number;
}
type FormHandlers = EventHandlers<FormFields>;
// { onName: (value: string) => void; onEmail: (value: string) => void; onAge: (value: number) => void; }
Discriminated Unions
Pattern Matching
interface Loading {
status: 'loading';
}
interface Success<T> {
status: 'success';
data: T;
}
interface Error {
status: 'error';
error: string;
}
type AsyncState<T> = Loading | Success<T> | Error;
function handleState<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'success':
return `Data: ${JSON.stringify(state.data)}`;
case 'error':
return `Error: ${state.error}`;
}
}
// Exhaustiveness checking
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
function handleStateExhaustive<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'success':
return `Data: ${JSON.stringify(state.data)}`;
case 'error':
return `Error: ${state.error}`;
default:
return assertNever(state); // Compile error if case missing
}
}
Type Guards
Custom Type Guards
interface Cat {
meow(): void;
}
interface Dog {
bark(): void;
}
// Type predicate
function isCat(animal: Cat | Dog): animal is Cat {
return 'meow' in animal;
}
function makeSound(animal: Cat | Dog) {
if (isCat(animal)) {
animal.meow(); // TypeScript knows it's Cat
} else {
animal.bark(); // TypeScript knows it's Dog
}
}
// Assert function (throws if false)
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value is not a string');
}
}
function processValue(value: unknown) {
assertIsString(value);
// TypeScript knows value is string here
console.log(value.toUpperCase());
}
Declaration Merging
Extending Existing Types
// Extend Window
declare global {
interface Window {
analytics: {
track: (event: string, data?: object) => void;
};
}
}
// Now valid
window.analytics.track('pageview');
// Extend Express Request
declare namespace Express {
interface Request {
user?: {
id: string;
role: string;
};
}
}
// Extend module
declare module 'express' {
interface Request {
customProperty: string;
}
}
Practical Patterns
Builder Pattern with Types
class RequestBuilder<T extends object = {}> {
private config: T = {} as T;
withUrl<U extends string>(url: U): RequestBuilder<T & { url: U }> {
return Object.assign(this, { config: { ...this.config, url } });
}
withMethod<M extends 'GET' | 'POST'>(method: M): RequestBuilder<T & { method: M }> {
return Object.assign(this, { config: { ...this.config, method } });
}
build(): T {
return this.config;
}
}
const request = new RequestBuilder()
.withUrl('/api/users')
.withMethod('GET')
.build();
// Type: { url: '/api/users'; method: 'GET'; }
Key Takeaways
- Use utility types: Don't reinvent Partial, Pick, Omit
- Constrain generics:
extends makes types more useful - Discriminated unions: Powerful pattern matching
- Type guards: Narrow types safely
- Template literals: Type-safe string manipulation
- Mapped types: Transform type shapes systematically
TypeScript's type system is a language within a language—invest in learning it deeply, and your code quality will improve dramatically.