Careeroza
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.