Advanced TypeScript Patterns for Enterprise Applications
TypeScript has evolved far beyond simple type annotations. In enterprise applications, advanced TypeScript patterns can provide incredible type safety, better developer experience, and more maintainable code. After working with TypeScript in large-scale applications for several years, I'm sharing the most powerful patterns that have transformed how I write and maintain complex codebases.
Conditional Types and Type-Level Programming
Conditional types allow you to create types that change based on conditions, enabling powerful type-level logic.
Basic Conditional Types
// Basic conditional type syntax
type IsString<T> = T extends string ? true : false
type Test1 = IsString<string> // true
type Test2 = IsString<number> // false
// More practical example: Extract function return types
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type FunctionReturn = ReturnType<() => string> // string
type MethodReturn = ReturnType<(x: number) => boolean> // boolean
Advanced Conditional Type Patterns
// Recursive conditional types for deep object manipulation
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P]
: DeepReadonly<T[P]>
: T[P]
}
interface User {
id: number
profile: {
name: string
settings: {
theme: string
notifications: boolean
}
}
updateProfile: (data: any) => void
}
type ReadonlyUser = DeepReadonly<User>
// Result: All properties are readonly except functions
// Conditional type for API response handling
type ApiResponse<T> = T extends { error: any }
? { success: false; error: T["error"] }
: { success: true; data: T }
type UserResponse = ApiResponse<{ id: number; name: string }>
// { success: true; data: { id: number; name: string } }
type ErrorResponse = ApiResponse<{ error: string }>
// { success: false; error: string }
Template Literal Types
Template literal types provide powerful string manipulation at the type level.
Dynamic API Endpoint Types
// Generate API endpoint types dynamically
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"
type ApiVersion = "v1" | "v2"
type Resource = "users" | "posts" | "comments"
type ApiEndpoint<V extends ApiVersion, R extends Resource> = `/api/${V}/${R}`
type UserEndpoints = ApiEndpoint<"v1", "users"> // "/api/v1/users"
type PostEndpoints = ApiEndpoint<"v2", "posts"> // "/api/v2/posts"
// Advanced template literal patterns
type EventName<T extends string> = `on${Capitalize<T>}`
type EventHandler<T extends string> = `handle${Capitalize<T>}`
type ClickEvent = EventName<"click"> // "onClick"
type ClickHandler = EventHandler<"click"> // "handleClick"
// CSS-in-JS type safety
type CSSProperty = "margin" | "padding" | "border"
type CSSDirection = "top" | "right" | "bottom" | "left"
type CSSPropertyWithDirection<
P extends CSSProperty,
D extends CSSDirection,
> = `${P}-${D}`
type MarginProperties = CSSPropertyWithDirection<"margin", CSSDirection>
// "margin-top" | "margin-right" | "margin-bottom" | "margin-left"
Form Validation with Template Literals
// Type-safe form field validation
type ValidationRule = "required" | "email" | "minLength" | "maxLength"
type FieldName = "username" | "email" | "password"
type ValidationKey<F extends FieldName, R extends ValidationRule> = `${F}_${R}`
type ValidationErrors = {
[K in ValidationKey<FieldName, ValidationRule>]?: string
}
// Usage
const errors: ValidationErrors = {
username_required: "Username is required",
email_email: "Invalid email format",
password_minLength: "Password must be at least 8 characters",
}
// Type-safe validation function
function validateField<F extends FieldName, R extends ValidationRule>(
field: F,
rule: R,
value: string,
): ValidationKey<F, R> | null {
// Validation logic here
return null // or return the validation key if validation fails
}
Mapped Types and Key Remapping
Mapped types allow you to transform existing types by iterating over their properties.
Advanced Mapped Type Patterns
// Optional properties with specific keys
type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
interface User {
id: number
name: string
email: string
avatar?: string
}
type UserUpdate = OptionalKeys<User, "name" | "email">
// { id: number; avatar?: string; name?: string; email?: string; }
// Transform object properties
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
}
type UserGetters = Getters<User>
// { getId: () => number; getName: () => string; getEmail: () => string; getAvatar: () => string | undefined; }
type UserSetters = Setters<User>
// { setId: (value: number) => void; setName: (value: string) => void; ... }
// Combine getters and setters
type Model<T> = T & Getters<T> & Setters<T>
type UserModel = Model<User>
Database Query Builder Types
// Type-safe query builder
type QueryOperator = "eq" | "ne" | "gt" | "lt" | "in" | "like"
type WhereClause<T> = {
[K in keyof T]?: {
[O in QueryOperator]?: T[K] extends (infer U)[] ? U : T[K]
}
}
type OrderByClause<T> = {
[K in keyof T]?: "asc" | "desc"
}
interface QueryBuilder<T> {
where(clause: WhereClause<T>): QueryBuilder<T>
orderBy(clause: OrderByClause<T>): QueryBuilder<T>
limit(count: number): QueryBuilder<T>
execute(): Promise<T[]>
}
// Usage
interface Product {
id: number
name: string
price: number
category: string
tags: string[]
}
declare function createQuery<T>(): QueryBuilder<T>
const products = await createQuery<Product>()
.where({
price: { gt: 100, lt: 500 },
category: { eq: "electronics" },
tags: { in: "smartphone" },
})
.orderBy({ price: "asc" })
.limit(10)
.execute()
Utility Types and Type Manipulation
Custom Utility Types
// Deep partial for nested objects
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// Non-nullable type
type NonNullable<T> = T extends null | undefined ? never : T
// Extract keys by value type
type KeysOfType<T, U> = {
[K in keyof T]: T[K] extends U ? K : never
}[keyof T]
interface Example {
id: number
name: string
isActive: boolean
metadata: object
count: number
}
type StringKeys = KeysOfType<Example, string> // "name"
type NumberKeys = KeysOfType<Example, number> // "id" | "count"
// Require at least one property
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<
T,
Exclude<keyof T, Keys>
> &
{
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
}[Keys]
type UpdateUser = RequireAtLeastOne<
{ name?: string; email?: string; avatar?: string },
"name" | "email"
>
// Valid: { name: "John" }, { email: "john@example.com" }, { name: "John", email: "john@example.com" }
// Invalid: { avatar: "avatar.jpg" } - must have at least name or email
Function Type Utilities
// Extract function parameters
type Parameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never
// Create overloaded function types
type OverloadedFunction<T extends Record<string, (...args: any[]) => any>> = {
[K in keyof T]: T[K]
}[keyof T]
interface ApiMethods {
getUser: (id: number) => Promise<User>
getUsers: (filters?: UserFilters) => Promise<User[]>
createUser: (data: CreateUserData) => Promise<User>
}
type ApiFunction = OverloadedFunction<ApiMethods>
// (id: number) => Promise<User> | (filters?: UserFilters) => Promise<User[]> | (data: CreateUserData) => Promise<User>
// Async function return type
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
...args: any
) => Promise<infer R>
? R
: never
type UserData = AsyncReturnType<ApiMethods["getUser"]> // User
Design Patterns with TypeScript
Builder Pattern with Type Safety
// Type-safe builder pattern
interface DatabaseConfig {
host: string
port: number
database: string
username: string
password: string
ssl?: boolean
timeout?: number
}
type RequiredKeys = "host" | "port" | "database" | "username" | "password"
type OptionalKeys = "ssl" | "timeout"
class DatabaseConfigBuilder {
private config: Partial<DatabaseConfig> = {}
host(
host: string,
): Omit<DatabaseConfigBuilder, "host"> & { host: () => never } {
this.config.host = host
return this as any
}
port(
port: number,
): Omit<DatabaseConfigBuilder, "port"> & { port: () => never } {
this.config.port = port
return this as any
}
database(
database: string,
): Omit<DatabaseConfigBuilder, "database"> & { database: () => never } {
this.config.database = database
return this as any
}
username(
username: string,
): Omit<DatabaseConfigBuilder, "username"> & { username: () => never } {
this.config.username = username
return this as any
}
password(
password: string,
): Omit<DatabaseConfigBuilder, "password"> & { password: () => never } {
this.config.password = password
return this as any
}
ssl(ssl: boolean): this {
this.config.ssl = ssl
return this
}
timeout(timeout: number): this {
this.config.timeout = timeout
return this
}
build(): DatabaseConfig {
const requiredKeys: RequiredKeys[] = [
"host",
"port",
"database",
"username",
"password",
]
for (const key of requiredKeys) {
if (!(key in this.config)) {
throw new Error(`Missing required configuration: ${key}`)
}
}
return this.config as DatabaseConfig
}
}
// Usage - TypeScript enforces all required methods are called
const config = new DatabaseConfigBuilder()
.host("localhost")
.port(5432)
.database("myapp")
.username("user")
.password("password")
.ssl(true)
.build()
Factory Pattern with Generic Constraints
// Generic factory with constraints
interface Entity {
id: string
createdAt: Date
updatedAt: Date
}
interface User extends Entity {
name: string
email: string
}
interface Product extends Entity {
name: string
price: number
category: string
}
type EntityConstructor<T extends Entity> = new (
data: Omit<T, keyof Entity>,
) => T
class EntityFactory<T extends Entity> {
constructor(private EntityClass: EntityConstructor<T>) {}
create(data: Omit<T, keyof Entity>): T {
return new this.EntityClass(data)
}
createMany(dataArray: Omit<T, keyof Entity>[]): T[] {
return dataArray.map((data) => this.create(data))
}
}
// Implementation
class UserEntity implements User {
id: string
createdAt: Date
updatedAt: Date
name: string
email: string
constructor(data: Omit<User, keyof Entity>) {
this.id = generateId()
this.createdAt = new Date()
this.updatedAt = new Date()
this.name = data.name
this.email = data.email
}
}
// Usage
const userFactory = new EntityFactory(UserEntity)
const user = userFactory.create({ name: "John", email: "john@example.com" })
Observer Pattern with Type Safety
// Type-safe observer pattern
type EventMap = {
userCreated: { user: User }
userUpdated: { user: User; changes: Partial<User> }
userDeleted: { userId: string }
}
type EventListener<T> = (data: T) => void | Promise<void>
class TypedEventEmitter<T extends Record<string, any>> {
private listeners: {
[K in keyof T]?: EventListener<T[K]>[]
} = {}
on<K extends keyof T>(event: K, listener: EventListener<T[K]>): void {
if (!this.listeners[event]) {
this.listeners[event] = []
}
this.listeners[event]!.push(listener)
}
off<K extends keyof T>(event: K, listener: EventListener<T[K]>): void {
const eventListeners = this.listeners[event]
if (eventListeners) {
const index = eventListeners.indexOf(listener)
if (index > -1) {
eventListeners.splice(index, 1)
}
}
}
emit<K extends keyof T>(event: K, data: T[K]): void {
const eventListeners = this.listeners[event]
if (eventListeners) {
eventListeners.forEach((listener) => listener(data))
}
}
}
// Usage
const eventEmitter = new TypedEventEmitter<EventMap>()
eventEmitter.on("userCreated", ({ user }) => {
console.log(`User created: ${user.name}`)
})
eventEmitter.on("userUpdated", ({ user, changes }) => {
console.log(`User ${user.name} updated:`, changes)
})
// Type-safe emission
eventEmitter.emit("userCreated", { user: newUser })
eventEmitter.emit("userUpdated", {
user: updatedUser,
changes: { name: "New Name" },
})
Advanced Generic Patterns
Variance and Generic Constraints
// Covariance and contravariance
interface Producer<out T> {
produce(): T
}
interface Consumer<in T> {
consume(item: T): void
}
interface Processor<T> {
process(input: T): T
}
// Generic constraints with multiple bounds
interface Identifiable {
id: string
}
interface Timestamped {
createdAt: Date
updatedAt: Date
}
function processEntity<T extends Identifiable & Timestamped>(
entity: T,
processor: (item: T) => T,
): T {
const processed = processor(entity)
return {
...processed,
updatedAt: new Date(),
}
}
// Conditional generic constraints
type ApiEndpoint<T> = T extends { id: infer ID }
? {
get: (id: ID) => Promise<T>
update: (id: ID, data: Partial<T>) => Promise<T>
delete: (id: ID) => Promise<void>
}
: {
create: (data: T) => Promise<T & { id: string }>
list: () => Promise<T[]>
}
type UserEndpoint = ApiEndpoint<User> // Has get, update, delete methods
type CreateUserEndpoint = ApiEndpoint<Omit<User, "id">> // Has create, list methods
Higher-Order Type Functions
// Type-level functions
type Curry<T> = T extends (arg1: infer A, ...rest: infer R) => infer Return
? R extends []
? (arg1: A) => Return
: (arg1: A) => Curry<(...rest: R) => Return>
: never
type Add = (a: number, b: number, c: number) => number
type CurriedAdd = Curry<Add> // (a: number) => (b: number) => (c: number) => number
// Compose function types
type Compose<F, G> = F extends (arg: infer A) => infer B
? G extends (arg: B) => infer C
? (arg: A) => C
: never
: never
type StringToNumber = (s: string) => number
type NumberToBoolean = (n: number) => boolean
type StringToBoolean = Compose<StringToNumber, NumberToBoolean> // (s: string) => boolean
// Pipeline type
type Pipe<T, Fns extends readonly any[]> = Fns extends readonly [
(arg: T) => infer U,
...infer Rest,
]
? Rest extends readonly any[]
? Pipe<U, Rest>
: U
: T
type PipelineResult = Pipe<
string,
[(s: string) => number, (n: number) => boolean, (b: boolean) => string]
> // string
Real-World Application: Type-Safe API Client
// Complete type-safe API client implementation
interface ApiSchema {
"/users": {
GET: {
query?: { page?: number; limit?: number }
response: { users: User[]; total: number }
}
POST: {
body: Omit<User, "id" | "createdAt" | "updatedAt">
response: User
}
}
"/users/:id": {
GET: {
params: { id: string }
response: User
}
PUT: {
params: { id: string }
body: Partial<Omit<User, "id" | "createdAt" | "updatedAt">>
response: User
}
DELETE: {
params: { id: string }
response: void
}
}
}
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & ExtractParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {}
type ApiMethod<
Schema extends Record<string, any>,
Path extends keyof Schema,
Method extends keyof Schema[Path],
> = Schema[Path][Method] extends {
params: infer P
query?: infer Q
body?: infer B
response: infer R
}
? (params: P, options?: { query?: Q; body?: B }) => Promise<R>
: Schema[Path][Method] extends {
query?: infer Q
body?: infer B
response: infer R
}
? (options?: { query?: Q; body?: B }) => Promise<R>
: Schema[Path][Method] extends { response: infer R }
? () => Promise<R>
: never
class TypedApiClient<Schema extends Record<string, any>> {
constructor(private baseUrl: string) {}
request<Path extends keyof Schema, Method extends keyof Schema[Path]>(
method: Method,
path: Path,
...args: Parameters<ApiMethod<Schema, Path, Method>>
): ReturnType<ApiMethod<Schema, Path, Method>> {
// Implementation here
return null as any
}
get<Path extends keyof Schema>(
path: Path,
...args: Parameters<ApiMethod<Schema, Path, "GET">>
) {
return this.request("GET", path, ...args)
}
post<Path extends keyof Schema>(
path: Path,
...args: Parameters<ApiMethod<Schema, Path, "POST">>
) {
return this.request("POST", path, ...args)
}
put<Path extends keyof Schema>(
path: Path,
...args: Parameters<ApiMethod<Schema, Path, "PUT">>
) {
return this.request("PUT", path, ...args)
}
delete<Path extends keyof Schema>(
path: Path,
...args: Parameters<ApiMethod<Schema, Path, "DELETE">>
) {
return this.request("DELETE", path, ...args)
}
}
// Usage
const api = new TypedApiClient<ApiSchema>("https://api.example.com")
// Type-safe API calls
const users = await api.get("/users", { query: { page: 1, limit: 10 } })
const user = await api.get("/users/:id", { id: "123" })
const newUser = await api.post("/users", {
body: { name: "John", email: "john@example.com" },
})
Conclusion
Advanced TypeScript patterns provide powerful tools for building robust, maintainable enterprise applications. These patterns offer:
- Enhanced Type Safety - Catch errors at compile time rather than runtime
- Better Developer Experience - Intelligent autocomplete and refactoring support
- Self-Documenting Code - Types serve as living documentation
- Reduced Bugs - Impossible states become unrepresentable
- Improved Maintainability - Changes propagate through the type system
The key to mastering these patterns is to start simple and gradually incorporate more advanced techniques as your understanding grows. Focus on solving real problems in your codebase rather than using complex types for their own sake.
Remember: the goal is not to write the most complex types possible, but to create types that make your code more reliable, maintainable, and enjoyable to work with.
Which of these patterns have you found most useful in your projects? Are there specific TypeScript challenges you're facing that these patterns could help solve? I'd love to hear about your experiences and help with any advanced TypeScript questions you might have.