wmercer
wmercer

Reputation: 1152

Builder pattern using TypeScript interfaces

I would like to do something like this:

interface IPoint {
    x : number;
    y : number;
    z? : number;
}
const diag : IPoint = IPoint.x(1)
                            .y(2)
                            .build();

I realize I could implement this myself, but was wondering if there was an automatic way to do this? given TypeScript already knows the type information.

Edit: I am requesting this kind of syntax because I can do this currently.

const diag : IPoint = {x: 1, y: 1};

Upvotes: 12

Views: 19080

Answers (3)

unional
unional

Reputation: 15589

This handles the type:

interface IPoint {
    x: number;
    y: number;
    z?: number;
}

type IBuilder<T> = {
    [k in keyof T]: (arg: T[k]) => IBuilder<T>
} & { build(): T }


let builder = {} as IBuilder<IPoint>

const diag = builder.x(1).y(2).z(undefined).build()

But I don't know how will you create the actual Builder thou. :)

You can play around with it at the playground

EDIT: Vincent Peng has created a builder-pattern npm package out of this (as mentioned in the comment). Go and give it some love!

Upvotes: 20

Andreas Hellmann
Andreas Hellmann

Reputation: 31

Based on the previous answers I wrote a generic Typescript builder which provides:

  • Type-Safety
  • Differentiation of optional and required properties
  • Generic method (with) for adding parts of the object
  • Validation of the object before creating it
  • Domain Driven Design conformity (no need to specify methods when using Builder)

If you are interested, you can find further information and examples here: https://github.com/hanterlantant/ts-generic-builder And the npm package here: https://www.npmjs.com/package/ts-generic-builder

Upvotes: 3

Karol Majewski
Karol Majewski

Reputation: 25790

The following design adds type-safety by accomplishing 3 things:

  1. It is aware which of the required properties have been already provided.
  2. It is aware which of the optional properties have been already provided.
  3. Will only let you build once you have provided all required properties.

The Point itself:

interface Point {
  x: number;
  y: number;
  z?: number;
}

class Point implements Point {
  constructor(point: Point) {
    Object.assign(this, point);
  }
}

The Point builder:

class PointBuilder implements Partial<Point> {
  x?: number;
  y?: number;
  z?: number;

  withX(value: number): this & Pick<Point, 'x'> {
    return Object.assign(this, { x: value });
  }

  withY(value: number): this & Pick<Point, 'y'> {
    return Object.assign(this, { y: value });
  }

  withZ(value: number): this & Required<Pick<Point, 'z'>> {
    return Object.assign(this, { z: value });
  }

  build(this: Point) {
    return new Point(this);
  }
}

Usage:

/**
 * The `z` property is optional.
 */
new PointBuilder()
  .withX(1)
  .withY(1)
  .build();

/**
 * The `.build()` method cannot be called — we are still missing `y`.
 */
new PointBuilder()
  .withX(1)
  .withZ(1);

/**
 * The `z` property is correctly recognized as `number` (as opposed to `number | undefined`).
 */
new PointBuilder()
  .withX(1)
  .withZ(1)
  .z

Upvotes: 26

Related Questions