TypeScript's type system is powerful enough to catch bugs at compile time that would otherwise reach production. Here are advanced patterns that make your code safer and more expressive.
Discriminated Unions#
1// Type-safe state handling
2type LoadingState = { status: 'loading' };
3type SuccessState<T> = { status: 'success'; data: T };
4type ErrorState = { status: 'error'; error: Error };
5
6type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
7
8function handleState<T>(state: AsyncState<T>): string {
9 switch (state.status) {
10 case 'loading':
11 return 'Loading...';
12 case 'success':
13 return `Data: ${state.data}`; // TypeScript knows data exists
14 case 'error':
15 return `Error: ${state.error.message}`; // TypeScript knows error exists
16 }
17}
18
19// Exhaustive checking
20function assertNever(x: never): never {
21 throw new Error(`Unexpected value: ${x}`);
22}
23
24function handleStateExhaustive<T>(state: AsyncState<T>): string {
25 switch (state.status) {
26 case 'loading':
27 return 'Loading...';
28 case 'success':
29 return `Data: ${state.data}`;
30 case 'error':
31 return `Error: ${state.error.message}`;
32 default:
33 return assertNever(state); // Compiler error if case missing
34 }
35}Template Literal Types#
1// Type-safe event names
2type EventName = `on${Capitalize<string>}`;
3type ValidEvent = 'onClick' | 'onHover' | 'onSubmit';
4
5// CSS units
6type CSSUnit = 'px' | 'rem' | 'em' | '%';
7type CSSValue = `${number}${CSSUnit}`;
8
9const padding: CSSValue = '16px'; // ✅
10const margin: CSSValue = '1.5rem'; // ✅
11// const invalid: CSSValue = '16'; // ❌ Error
12
13// API routes
14type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
15type ApiRoute = `/${string}`;
16type ApiEndpoint = `${HttpMethod} ${ApiRoute}`;
17
18const endpoint: ApiEndpoint = 'GET /users'; // ✅
19const createUser: ApiEndpoint = 'POST /users'; // ✅Conditional Types#
1// Extract return type
2type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
3
4// Extract promise value
5type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
6
7// Filter array type
8type Filter<T, U> = T extends U ? T : never;
9
10type Numbers = Filter<string | number | boolean, number>; // number
11
12// Distributive conditional types
13type ToArray<T> = T extends any ? T[] : never;
14type StrOrNumArray = ToArray<string | number>; // string[] | number[]
15
16// Non-distributive
17type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
18type StrOrNumArray2 = ToArrayNonDist<string | number>; // (string | number)[]Mapped Types#
1// Make all properties optional
2type Partial<T> = { [P in keyof T]?: T[P] };
3
4// Make all properties required
5type Required<T> = { [P in keyof T]-?: T[P] };
6
7// Make all properties readonly
8type Readonly<T> = { readonly [P in keyof T]: T[P] };
9
10// Pick specific properties
11type Pick<T, K extends keyof T> = { [P in K]: T[P] };
12
13// Transform property types
14type Nullable<T> = { [P in keyof T]: T[P] | null };
15
16// Rename properties
17type Getters<T> = {
18 [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
19};
20
21interface User {
22 name: string;
23 age: number;
24}
25
26type UserGetters = Getters<User>;
27// { getName: () => string; getAge: () => number }Type-Safe Builder Pattern#
1class QueryBuilder<T extends object = {}> {
2 private query: T = {} as T;
3
4 select<K extends string>(
5 fields: K[]
6 ): QueryBuilder<T & { select: K[] }> {
7 (this.query as any).select = fields;
8 return this as any;
9 }
10
11 where<W extends object>(
12 conditions: W
13 ): QueryBuilder<T & { where: W }> {
14 (this.query as any).where = conditions;
15 return this as any;
16 }
17
18 limit(n: number): QueryBuilder<T & { limit: number }> {
19 (this.query as any).limit = n;
20 return this as any;
21 }
22
23 build(): T {
24 return this.query;
25 }
26}
27
28const query = new QueryBuilder()
29 .select(['name', 'email'])
30 .where({ status: 'active' })
31 .limit(10)
32 .build();
33
34// query type: { select: string[]; where: { status: string }; limit: number }Type-Safe API Client#
1// Define API schema
2interface ApiSchema {
3 '/users': {
4 GET: { response: User[] };
5 POST: { body: CreateUser; response: User };
6 };
7 '/users/:id': {
8 GET: { response: User };
9 PUT: { body: UpdateUser; response: User };
10 DELETE: { response: void };
11 };
12}
13
14type PathParams<T extends string> =
15 T extends `${infer _Start}:${infer Param}/${infer Rest}`
16 ? { [K in Param | keyof PathParams<Rest>]: string }
17 : T extends `${infer _Start}:${infer Param}`
18 ? { [K in Param]: string }
19 : {};
20
21class ApiClient {
22 async request<
23 Path extends keyof ApiSchema,
24 Method extends keyof ApiSchema[Path]
25 >(
26 method: Method,
27 path: Path,
28 options?: {
29 params?: PathParams<Path & string>;
30 body?: ApiSchema[Path][Method] extends { body: infer B } ? B : never;
31 }
32 ): Promise<
33 ApiSchema[Path][Method] extends { response: infer R } ? R : never
34 > {
35 // Implementation
36 return {} as any;
37 }
38}
39
40const client = new ApiClient();
41
42// Type-safe API calls
43const users = await client.request('GET', '/users');
44// users: User[]
45
46const user = await client.request('GET', '/users/:id', {
47 params: { id: '123' },
48});
49// user: User
50
51await client.request('POST', '/users', {
52 body: { name: 'John', email: 'john@example.com' },
53});Branded Types#
1// Prevent mixing similar types
2declare const brand: unique symbol;
3
4type Brand<T, B> = T & { [brand]: B };
5
6type UserId = Brand<string, 'UserId'>;
7type PostId = Brand<string, 'PostId'>;
8
9function createUserId(id: string): UserId {
10 return id as UserId;
11}
12
13function createPostId(id: string): PostId {
14 return id as PostId;
15}
16
17function getUser(id: UserId): User {
18 // ...
19}
20
21const userId = createUserId('user-123');
22const postId = createPostId('post-456');
23
24getUser(userId); // ✅
25// getUser(postId); // ❌ Type error!
26// getUser('raw-string'); // ❌ Type error!Type Guards#
1// Custom type guards
2function isString(value: unknown): value is string {
3 return typeof value === 'string';
4}
5
6function isUser(value: unknown): value is User {
7 return (
8 typeof value === 'object' &&
9 value !== null &&
10 'id' in value &&
11 'email' in value
12 );
13}
14
15// Assertion functions
16function assertIsUser(value: unknown): asserts value is User {
17 if (!isUser(value)) {
18 throw new Error('Value is not a User');
19 }
20}
21
22function processValue(value: unknown) {
23 assertIsUser(value);
24 // TypeScript now knows value is User
25 console.log(value.email);
26}Utility Types#
1// Deep partial
2type DeepPartial<T> = {
3 [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
4};
5
6// Deep readonly
7type DeepReadonly<T> = {
8 readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
9};
10
11// Exact type (no extra properties)
12type Exact<T, U extends T> = T & {
13 [K in Exclude<keyof U, keyof T>]: never;
14};
15
16// NonNullable nested
17type DeepNonNullable<T> = {
18 [P in keyof T]: NonNullable<T[P]> extends object
19 ? DeepNonNullable<NonNullable<T[P]>>
20 : NonNullable<T[P]>;
21};Best Practices#
DO:
✓ Use discriminated unions for state
✓ Prefer type inference where clear
✓ Use branded types for domain safety
✓ Write custom type guards
✓ Use template literals for patterns
DON'T:
✗ Overuse 'any' or 'as'
✗ Create overly complex types
✗ Ignore compiler errors
✗ Skip type documentation
Conclusion#
Advanced TypeScript patterns encode business logic in the type system. Discriminated unions handle state, branded types prevent mixing, and conditional types enable flexible APIs.
The goal is catching bugs at compile time—before they reach production.