Clean Code

Method Chaining in TypeScript: Best Practices and Examples

I was building a query builder. Without method chaining, every operation needed a temporary variable. Five lines of boilerplate to express a single query....

19 Apr 2024

Method Chaining in TypeScript: Best Practices and Examples

I was building a query builder. Without method chaining, every operation needed a temporary variable. Five lines of boilerplate to express a single query. With chaining, the same query read like a sentence.

Method chaining lets you call multiple methods on the same object in sequence, each returning the object so the next call can follow. It turns imperative setup code into a fluent, readable pipeline.

How it works

The trick is simple: return this from every method that modifies the object.

Typescript
class StringBuilder {
  private value: string;

  constructor(value: string = '') {
    this.value = value;
  }

  append(text: string): StringBuilder {
    this.value += text;
    return this;
  }

  prepend(text: string): StringBuilder {
    this.value = text + this.value;
    return this;
  }

  capitalize(): StringBuilder {
    this.value = this.value.charAt(0).toUpperCase() + this.value.slice(1);
    return this;
  }

  toString(): string {
    return this.value;
  }
}

const result = new StringBuilder()
  .append('hello, ')
  .prepend('Hi, ')
  .capitalize()
  .toString();

console.log(result); // "Hi, hello, "

Each method returns the same StringBuilder instance. The caller chains calls without storing intermediate variables.

Where chaining shines

Query builders. db.select('*').from('users').where('active', true).limit(10) reads like a description of what you want.

Configuration. new Server().port(3000).cors(true).logging('debug').start() is clearer than setting five separate properties.

Transformations. Any pipeline where data flows through a sequence of operations benefits from chaining.

Guidelines for good chaining

Return this for mutating methods. Any method that modifies the object and doesn't need to return a value should return this instead.

End chains with a terminal method. Chain building methods. End with a method that produces the final result: toString(), build(), execute(), toArray().

Keep side effects out of the chain. Each chained method should transform the object, not send emails or write to databases. Side effects in a chain are invisible and surprising.

Document chainability. If your API supports chaining, make it obvious. Return types in TypeScript make this self-documenting — when a method returns this, the IDE shows it.

The trade-off

Chaining makes code compact and readable when the chain is short. But long chains become hard to debug. You can't easily set a breakpoint on the third call in a chain. You can't inspect intermediate state without breaking the chain apart.

Error handling is also tricky. If prepend throws, the stack trace points to the entire chain expression, not the specific method.

Use chaining for configuration, building, and transformation. Avoid it when individual steps need debugging, error handling, or conditional logic. A chain of 15 methods is a code smell — break it into stages.