TypeScript Advanced Patterns
Deep dive into advanced TypeScript patterns for building type-safe, maintainable applications.
Type-Level Programming
TypeScript's type system is Turing complete. Here are some powerful patterns:
Conditional Types
TypeScript
1type IsString<T> = T extends string ? true : false;
2
3type A = IsString<"hello">; // true
4type B = IsString<42>; // false
Mapped Types
TypeScript
1type Readonly<T> = {
2 readonly [P in keyof T]: T[P];
3};
4
5type Partial<T> = {
6 [P in keyof T]?: T[P];
7};
Mapped types are the foundation of many utility types in TypeScript.
Template Literal Types
Building type-safe route handlers:
TypeScript
1type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
2type Route = '/users' | '/posts' | '/comments';
3
4type Endpoint = `${HTTPMethod} ${Route}`;
5// Results in: 'GET /users' | 'GET /posts' | 'POST /users' | ...
6
7function handleRequest(endpoint: Endpoint) {
8 // Fully type-safe!
9}
10
11handleRequest('GET /users'); // ✓
12handleRequest('GET /invalid'); // ✗ Type error
Branded Types
Preventing type confusion with nominal typing:
TypeScript
1type UserId = string & { readonly brand: unique symbol };
2type PostId = string & { readonly brand: unique symbol };
3
4function getUserId(id: string): UserId {
5 return id as UserId;
6}
7
8function getPost(postId: PostId) {
9 // Implementation
10}
11
12const userId = getUserId("user123");
13const postId = getUserId("post456") as unknown as PostId;
14
15getPost(userId); // ✗ Type error - cannot pass UserId where PostId expected
16getPost(postId); // ✓
Branded types caught 12 type confusion bugs in our codebase before production.
Builder Pattern with Fluent API
Type-safe builder with method chaining:
TypeScript
1class QueryBuilder<T extends object> {
2 private filters: Array<(item: T) => boolean> = [];
3
4 where<K extends keyof T>(
5 key: K,
6 value: T[K]
7 ): QueryBuilder<T> {
8 this.filters.push(item => item[key] === value);
9 return this;
10 }
11
12 execute(data: T[]): T[] {
13 return data.filter(item =>
14 this.filters.every(filter => filter(item))
15 );
16 }
17}
18
19// Usage
20const users = new QueryBuilder<User>()
21 .where('age', 25)
22 .where('status', 'active')
23 .execute(userList);
Recursive Types
Handling deeply nested structures:
TypeScript
1type DeepPartial<T> = {
2 [P in keyof T]?: T[P] extends object
3 ? DeepPartial<T[P]>
4 : T[P];
5};
6
7interface Config {
8 database: {
9 host: string;
10 port: number;
11 credentials: {
12 username: string;
13 password: string;
14 };
15 };
16}
17
18// All properties optional at any depth
19const partialConfig: DeepPartial<Config> = {
20 database: {
21 credentials: {
22 username: "admin"
23 // password is optional
24 }
25 }
26};
Type Guards with Type Predicates
TypeScript
1interface Dog {
2 type: 'dog';
3 bark(): void;
4}
5
6interface Cat {
7 type: 'cat';
8 meow(): void;
9}
10
11type Animal = Dog | Cat;
12
13function isDog(animal: Animal): animal is Dog {
14 return animal.type === 'dog';
15}
16
17function handleAnimal(animal: Animal) {
18 if (isDog(animal)) {
19 animal.bark(); // TypeScript knows it's a Dog
20 } else {
21 animal.meow(); // TypeScript knows it's a Cat
22 }
23}
Complex type-level programming can slow down the TypeScript compiler. Balance type safety with build performance.
Key Takeaways
- Type Safety: Advanced patterns catch bugs at compile time
- Developer Experience: Better autocomplete and IntelliSense
- Refactoring: Changes propagate through type system
- Documentation: Types serve as inline documentation
Placeholder content - replace with your actual TypeScript experiments.