Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Builder pattern using TypeScript interfaces

Tags:

typescript

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};
like image 267
wmercer Avatar asked Jul 24 '17 23:07

wmercer


3 Answers

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
like image 122
Karol Majewski Avatar answered Jan 02 '23 18:01

Karol Majewski


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 our of this (as mentioned in the comment). Go and give it some love!

like image 38
unional Avatar answered Jan 02 '23 17:01

unional


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

like image 32
Andreas Hellmann Avatar answered Jan 02 '23 17:01

Andreas Hellmann