Builder Pattern with Types

A Builder That Tracks What Was Set

A type parameter can record which steps have been called. build() declares a this type that requires the mandatory steps, so forgetting one is a compile error, not a runtime surprise.

Example

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

class RequestBuilder<Set extends string = never> {
  declare private readonly __set: { [K in Set]: true };
  private data: { url?: string; method?: HttpMethod; headers: Record<string, string>; body?: string } = { headers: {} };

  private next<S extends string>(): RequestBuilder<S> {
    return this as unknown as RequestBuilder<S>;
  }
  url(u: string): RequestBuilder<Set | "url"> {
    this.data.url = u;
    return this.next();
  }
  method(m: HttpMethod): RequestBuilder<Set | "method"> {
    this.data.method = m;
    return this.next();
  }
  header(k: string, v: string): RequestBuilder<Set> {
    this.data.headers[k] = v;
    return this;
  }
  json(body: object): RequestBuilder<Set> {
    this.data.body = JSON.stringify(body);
    return this.header("Content-Type", "application/json");
  }
  build(this: RequestBuilder<"url" | "method">): string {
    const { method, url, headers, body } = this.data;
    const hs = Object.entries(headers).map(([k, v]) => `${k}: ${v}`).join("\n");
    return [`${method} ${url}`, hs, body ?? ""].filter(Boolean).join("\n");
  }
}

const req = new RequestBuilder()
  .url("/api/orders")
  .header("Authorization", "Bearer t0k3n")
  .json({ productId: 42, qty: 1 })
  .method("POST")
  .build();
console.log(req);

Output

POST /api/orders
Authorization: Bearer t0k3n
Content-Type: application/json
{"productId":42,"qty":1}

Forgetting a Required Step

Example

Without .method(...), build() cannot be called (TS2684: the this context is not assignable).

class RequestBuilder<Set extends string = never> {
  declare private readonly __set: { [K in Set]: true };
  url(u: string): RequestBuilder<Set | "url"> { return this as unknown as RequestBuilder<Set | "url">; }
  method(m: string): RequestBuilder<Set | "method"> { return this as unknown as RequestBuilder<Set | "method">; }
  build(this: RequestBuilder<"url" | "method">): string { return "request"; }
}

new RequestBuilder().url("/api/orders").build();

Compiler output (tsc)

Line 8: error TS2684: The 'this' context of type 'RequestBuilder<"url">' is not assignable to method's 'this' of type 'RequestBuilder<"url" | "method">'.
  Types of property '__set' are incompatible.
    Property 'method' is missing in type '{ url: true; }' but required in type '{ url: true; method: true; }'.

Tip: declare private readonly __set is a phantom field: it exists only in the type (no JavaScript is emitted) and carries the "steps done" information.