Advancedtypescriptadvancedpatternsgenerics
Advanced TypeScript Patterns and Techniques
Master advanced TypeScript patterns including generics, conditional types, and utility types for building robust applications.
Badr
••6 min readPrerequisites
- ✓Solid TypeScript fundamentals
- ✓Experience with TypeScript generics
- ✓Understanding of JavaScript ES6+
Advanced TypeScript Patterns and Techniques
Take your TypeScript skills to the next level with advanced patterns that professional developers use to build type-safe, maintainable applications.
Advanced Generics
Generic Constraints with Multiple Types
interface Identifiable {
id: string;
}
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
function updateEntity<T extends Identifiable & Timestamped>(
entity: T,
updates: Partial<T>
): T {
return {
...entity,
...updates,
updatedAt: new Date(),
};
}
// Usage
const user = {
id: "123",
name: "Alice",
createdAt: new Date(),
updatedAt: new Date(),
};
const updated = updateEntity(user, { name: "Bob" });
Generic Factory Pattern
interface Constructor<T> {
new (...args: any[]): T;
}
class EntityFactory {
private static registry = new Map<string, Constructor<any>>();
static register<T>(name: string, ctor: Constructor<T>): void {
this.registry.set(name, ctor);
}
static create<T>(name: string, ...args: any[]): T {
const Ctor = this.registry.get(name);
if (!Ctor) {
throw new Error(`Entity ${name} not registered`);
}
return new Ctor(...args);
}
}
// Usage
class User {
constructor(public name: string) {}
}
EntityFactory.register("User", User);
const user = EntityFactory.create<User>("User", "Alice");
Conditional Types
Advanced Type Inference
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number
// More complex example
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
interface User {
profile: {
address: {
street: string;
city: string;
};
};
}
// All properties become optional recursively
const partialUser: DeepPartial<User> = {
profile: {
address: {
city: "NYC",
// street is optional
},
},
};
Distributive Conditional Types
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] (not (string | number)[])
// Practical example: Extract function types
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
interface MyInterface {
name: string;
age: number;
greet: () => void;
save: () => Promise<void>;
}
type Methods = FunctionKeys<MyInterface>; // 'greet' | 'save'
Template Literal Types
Type-Safe String Manipulation
type EventNames = "click" | "focus" | "blur";
type EventHandlers = `on${Capitalize<EventNames>}`;
// 'onClick' | 'onFocus' | 'onBlur'
// Route builder
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = "/users" | "/posts" | "/comments";
type Route = `${HTTPMethod} ${Endpoint}`;
// 'GET /users' | 'POST /users' | ...
// Implementation
function handleRoute(route: Route, handler: () => void): void {
// Type-safe routing
}
handleRoute("GET /users", () => {});
// handleRoute('INVALID /users', () => {}); // Error!
Path Type Generation
type PathImpl<T, K extends keyof T> = K extends string
? T[K] extends Record<string, any>
? T[K] extends ArrayLike<any>
? K | `${K}.${PathImpl<T[K], Exclude<keyof T[K], keyof any[]>>}`
: K | `${K}.${PathImpl<T[K], keyof T[K]>}`
: K
: never;
type Path<T> = PathImpl<T, keyof T> | keyof T;
interface DeepObject {
user: {
profile: {
name: string;
address: {
city: string;
};
};
};
}
type Paths = Path<DeepObject>;
// 'user' | 'user.profile' | 'user.profile.name' | 'user.profile.address' | 'user.profile.address.city'
// Type-safe path getter
function getPath<T, P extends Path<T>>(obj: T, path: P): any {
return path.split(".").reduce((acc, part) => acc?.[part], obj as any);
}
Mapped Type Modifiers
Advanced Property Mapping
// Remove readonly
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// Make specific properties required
type RequiredKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;
interface User {
id?: string;
name?: string;
email?: string;
}
type UserWithId = RequiredKeys<User, "id">;
// { id: string; name?: string; email?: string; }
Recursive Type Transformation
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type DeepNullable<T> = {
[K in keyof T]: T[K] extends object ? DeepNullable<T[K]> : T[K] | null;
};
interface Config {
database: {
host: string;
port: number;
};
api: {
key: string;
};
}
type NullableConfig = DeepNullable<Config>;
// All nested properties can be null
Type Guards & Assertion Functions
Advanced Type Narrowing
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
// Type predicate
function isCircle(shape: Shape): shape is Circle {
return shape.kind === "circle";
}
// Assertion function
function assertIsCircle(shape: Shape): asserts shape is Circle {
if (shape.kind !== "circle") {
throw new Error("Not a circle!");
}
}
function processShape(shape: Shape) {
assertIsCircle(shape);
// TypeScript knows shape is Circle here
console.log(shape.radius);
}
Builder Pattern with Type Safety
class QueryBuilder<T = any, Selected = never> {
private selectFields: string[] = [];
private whereConditions: any[] = [];
select<K extends keyof T>(...fields: K[]): QueryBuilder<T, Selected | K> {
this.selectFields.push(...(fields as string[]));
return this as any;
}
where(condition: Partial<T>): this {
this.whereConditions.push(condition);
return this;
}
build(): Pick<T, Selected extends keyof T ? Selected : never>[] {
// Build and execute query
return [] as any;
}
}
interface User {
id: string;
name: string;
email: string;
age: number;
}
// Type-safe query building
const result = new QueryBuilder<User>()
.select("name", "email")
.where({ age: 25 })
.build();
// result type: { name: string; email: string; }[]
// result.id // Error: Property 'id' does not exist
Branded Types
type Brand<K, T> = K & { __brand: T };
type USD = Brand<number, "USD">;
type EUR = Brand<number, "EUR">;
const usd = (amount: number): USD => amount as USD;
const eur = (amount: number): EUR => amount as EUR;
function convertUSDToEUR(amount: USD): EUR {
return eur(amount * 0.85);
}
const dollars = usd(100);
const euros = eur(100);
convertUSDToEUR(dollars); // OK
// convertUSDToEUR(euros); // Error: EUR is not assignable to USD
// convertUSDToEUR(100); // Error: number is not assignable to USD
Variance Annotations
interface Producer<out T> {
produce(): T;
}
interface Consumer<in T> {
consume(value: T): void;
}
interface Processor<in In, out Out> {
process(input: In): Out;
}
// Covariance
const stringProducer: Producer<string> = { produce: () => "hello" };
const anyProducer: Producer<any> = stringProducer; // OK
// Contravariance
const anyConsumer: Consumer<any> = { consume: (x) => {} };
const stringConsumer: Consumer<string> = anyConsumer; // OK
Best Practices
- Use discriminated unions for state management
- Leverage const assertions for literal types
- Prefer type inference over explicit annotations
- Use branded types for domain modeling
- Apply the builder pattern for complex configurations
Summary
You've mastered:
- ✅ Advanced generic patterns
- ✅ Conditional and mapped types
- ✅ Template literal types
- ✅ Type-safe builders
- ✅ Branded types for domain safety
These patterns will help you build robust, type-safe applications that scale!