builder
The createBuilder() function lets you create a builder whose methods can be defined after declaration (and even redefined) while keeping strict typing.
The idea: you declare a builder interface (method signatures), then you “plug” the implementation of these methods via handler.set(...). The builder is then usable via handler.use(accumulator) with fluent chaining.
Interactive example
Why is it easily overridable?
Methods are stored in a global store by builder name (builderName). This allows:
- defining methods in different modules (plugin-like)
- redefining a method later (the “last set wins”)
- having an instantiated builder (via
use) that always uses the store methods
Extension via interface override (“dynamic” methods)
In addition to runtime override (handler.set(...)), you can also extend the builder typing through TypeScript interface merging, which lets you add new methods “dynamically” (on the type side) from another module.
Example (plugin pattern):
typescript
// urlBuilder.ts
export interface UrlBuilder extends Builder<UrlAccumulator> {
path(segment: string): UrlBuilder;
build(): string;
}
export const urlBuilderHandler = createBuilder<UrlBuilder>("urlBuilder");typescript
// plugin-auth.ts (elsewhere)
import { urlBuilderHandler } from "./urlBuilder";
declare module "./urlBuilder" {
interface UrlBuilder {
auth(token: string): UrlBuilder;
}
}
urlBuilderHandler.set("auth", ({ args, accumulator, next }) => {
const [token] = args;
return next({
...accumulator,
query: { ...accumulator.query, token },
});
});The method becomes available both:
- at runtime (because you registered it via
set) - at compile time (because the
UrlBuilderinterface was augmented)
Syntax
typescript
interface Builder<
GenericAccumulator extends object = object
> {}
interface BuilderHandler<
GenericBuilder extends Builder = Builder
> {
set(
method: keyof GenericBuilder,
theFunction: (params: {
args: unknown[];
accumulator: object;
next(newAccumulator: object): unknown;
}) => unknown
): BuilderHandler<GenericBuilder>;
use(accumulator: object): GenericBuilder;
}
function createBuilder<
GenericBuilder extends Builder
>(
builderName: string
): BuilderHandler<GenericBuilder>;Usage notes
- Accumulator: this is the internal object your builder evolves. It is typed via
Builder<YourAccumulator>. - Chaining: for a method that returns the builder, the implementation must return
params.next(newAccumulator)(this lets the runtime know it must continue the chain). - Errors: calling a method not defined via
setthrows aMissingBuilderMethodsError.
See also
globalStore- Global per-process storagekind- Runtime/type tag used internally
