Home > database >  Constructor overload with and without destructured interface in TypeScript, to allow positional or n
Constructor overload with and without destructured interface in TypeScript, to allow positional or n

Time:06-26

I have a class with some required attributes and some optional attributes (which I want to be undefined when not set, as there's no default value). With optional args, there are a lot of parameters, so I would prefer to be able to name them for clarity, while without the optional args, I'd like to make it concise to construct. So, I want to be able to construct it using either named attributes in an object, or as positional arguments:

export interface ParamInterface {
  a: number;
  b: number;
  c: number;
  d?: number;
  e?: number;
  f?: number;
}

export class MyClass {
  a: number;
  b: number;
  c: number;
  d?: number;
  e?: number;
  f?: number;

  constructor({a, b, c, d, e, f}: ParamInterface);
  constructor(a: number, b: number, c: number, d?: number, e?: number, f?: number) {
    this.a = a;
    this.b = b;
    this.c = c;
    this.d = d;
    this.e = e;
    this.f = f;
  }
}

In other words, I want to be able to call:

new MyClass(1, 2, 3); // d, e, f will be undefined
new MyClass({a: 1, b: 2, c: 3}); // d, e, f will be undefined
new MyClass({a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}); // d, e, f will be set

And then I want this to raise an error:

new MyClass(1); // First three args required

When I do this, though, I get an error that This overload signature is not compatible with its implementation signature.

Is there any way to make it compatible, or another way to achieve what I'm after?

CodePudding user response:

Your constructor implementation type must be supertype of all declarations, like following:

export interface ParamInterface {
  a: number;
  b: number;
  c: number;
  d?: number;
  e?: number;
  f?: number;
}
export class MyClass {
  a: number;
  b: number;
  c: number;
  d?: number;
  e?: number;
  f?: number;

  constructor({ a, b, c, d, e, f }: ParamInterface);
  constructor(a: number, b: number, c: number, d?: number, e?: number, f?: number);
  constructor(...values: [ParamInterface] | [a: number, b: number, c: number, d?: number, e?: number, f?: number]) {
    if (values.length == 1) {
      const { a, b, c, d, e, f } = values[0];
      // assign values
    } else {
      const [a, b, c, d, e, f] = values;
      // assign values
    }
  }
}

Here we have two constructor declarations:

constructor({ a, b, c, d, e, f }: ParamInterface);
constructor(a: number, b: number, c: number, d?: number, e?: number, f?: number);

and one implementation:

constructor(...values: [ParamInterface] | [a: number, b: number, c: number, d?: number, e?: number, f?: number]) {
  // ...
}

Type declaration can be a little simplified:

export interface ParamInterface {
  a: number;
  b: number;
  c: number;
  d?: number;
  e?: number;
  f?: number;
}
type TupleParameters = [a: number, b: number, c: number, d?: number, e?: number, f?: number];

export class MyClass {
  // all fields

  constructor({ a, b, c, d, e, f }: ParamInterface);
  constructor(...values: TupleParameters);
  constructor(...values: [ParamInterface] | TupleParameters) {
    // unchanged
  }
}
  • Related