Type-Safe Query Builder with Method Chaining

mediumTypeScript

Lesson

Type-Safe Builder Pattern with TypeScript

The builder pattern is a creational design pattern that constructs complex objects step by step. When combined with TypeScript's type system, we can create "type-safe builders" that enforce method call order and prevent duplicate operations at compile time.

Traditional builders rely on runtime checks or documentation to ensure methods are called in the correct order. Type-safe builders use TypeScript's generic types and conditional types to encode state information directly into the type system. This means invalid method chains are caught during development, not at runtime.

The key insight is using branded types or literal types to track what methods have been called. Each method returns a new type that represents the updated state. For example, after calling .select(), the returned type knows that select has been called and can restrict which methods are available next.

TypeScript's conditional types (T extends U ? X : Y) are crucial here. They allow methods to have different return types based on the current state. We can also use intersection types (A & B) to combine multiple pieces of state information.

Another important technique is using this parameter constraints. By typing the this parameter, we can make methods only callable when the builder is in a specific state. This creates compile-time errors when methods are called out of order or multiple times.

The result is a fluent API that guides developers toward correct usage while catching mistakes before code runs.

Example
1// Example: Type-safe HTTP request builder 2type HasUrl = { _hasUrl: true }; 3type HasMethod = { _hasMethod: true }; 4 5class RequestBuilder<State = {}> { 6 private config = { url: '', method: 'GET', headers: {} }; 7 8 url(this: State extends HasUrl ? never : RequestBuilder<State>, path: string) { 9 this.config.url = path; 10 return this as RequestBuilder<State & HasUrl>; 11 } 12 13 method(this: State extends HasMethod ? never : RequestBuilder<State>, verb: string) { 14 this.config.method = verb; 15 return this as RequestBuilder<State & HasMethod>; 16 } 17 18 build(this: State extends HasUrl ? RequestBuilder<State> : never) { 19 return { ...this.config }; 20 } 21 22 static create() { 23 return new RequestBuilder(); 24 } 25}
L2Branded types track which methods have been called
L7Conditional type prevents calling url() twice
L17build() requires HasUrl state to be callable

Key Takeaways

  • •Branded types and intersection types can encode complex state information in TypeScript's type system
  • •Conditional types and `this` parameter constraints make methods available only when appropriate
  • •Type-safe builders catch API misuse at compile time rather than runtime
Loading...