0

I have a base class implementing a bunch of methods, each returning a new reference to a copy of object to allow chaining.

class Base {
  constructor(public name: string) {}
  funcA(): Base { return new Base('FUNC_A') }
  funcB(): Base { return new Base('FUNC_B') }
}

Additionally, I have a derived class, exposing only some of the methods of the base class, and also exposing its own methods. All of these methods should return a reference to the derived class object to allow for chaining.

class Derived extends Base {
  constructor() { super('DERIVED') }
  funcA(): Derived { return super.funcA() }
  newFunc(): Derived { return new Derived() }
}

I run into a problem in this case. The object returned by the overridden method is still an instance of the base class, whether it's casted as the derived class or not, and none of the methods defined for the first time in the derived class are defined.

One workaround comes to my mind, which is not elegant. Instead of inheritance, I could use composition to contain an instance of base class object inside a derived class object. However, for this, I need an additional constructor to accept a base class object which should be accessible outside the class, and use it like funcA(): Derived { return new Derived(this.baseObject) }.

Is there a more elegant way of solving this problem?

AweSIM
  • 1,651
  • 4
  • 18
  • 37

1 Answers1

2

I think this works in the way you intended:

class Base {
  constructor(public name: string) {}
  funcA(): this { return new (this.constructor as any)('FUNC_A') }
  funcB(): this { return new (this.constructor as any)('FUNC_B') }
}

class Derived extends Base {
  constructor() { super('DERIVED') }
  funcA(): this { return super.funcA() }
  newFunc(): this { return new (this.constructor as any)() }
}

Explanation:

Using funcA(): this instead of funcA(): Base tells TypeScript, that the function returns an object of the same type as the class that it is called on and not always Base. If called on an instance of Base it should return an instance of Base and if called on Derived it should return an instance of Derived.

To correctly implement that, calling new Base() would be wrong because it always returns an instance of base. To access the constructor of the actual instance, every javascript object has a this.constructor property (See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor). So I call new this.constructor() instead of new Base(). this.constructor will be Base if called on an instance of Base and Derived if called on an instance of Derived

Sadly typescript does not recoqnize this.constructor as the correct type, which would be a constructor of type new (...any[]) => this (See https://github.com/microsoft/TypeScript/issues/3841 ) so I just cast it to any. Maybe there is a better way to do that, that I don't know of.

If you have to call the constructor more often, you can create a method for it, so you can write it easier:

class Base {
  constructor(public name: string) {}
  newInstance(...args: any[]): this { return new (this.constructor as any)(...args); }
  funcA(): this { return this.newInstance('FUNC_A') }
  funcB(): this { return this.newInstance('FUNC_B') }
}

class Derived extends Base {
  constructor() { super('DERIVED') }
  funcA(): this { return super.funcA() }
  newFunc(): this { return this.newInstance() }
}

Here is the TS Playground I used for testing: https://www.typescriptlang.org/play?#code/MYGwhgzhAEBCkFNoG8BQ1rAPYDsIBcAnAV2Hy0IAoAHYgIxAEthocwBbBALmgMMZwBzAJQoAvulYIA7gEk8+MDmAJKAOg1hCgiDyUBPANoBdYT3wALRjGTRCCfMUI4p06JUvW12BSTIVoSECcfWF1TW0IYQBuaAkMADNiZQBBSjNoTxs7BycXLLUcGXkCJRVKAHIAMQBVADkAYQB9FIrReOgk5Vh08yts+0dnTP7C4oUy1Wr65tg2uNQJVFBIGAARBH4ANwQAE2gEAA98BBxdmHgIJDQMHz5ScipRWwhiak3KtYBRACVZADUvmt5h0usA0hksigckMXK93oQ1GCIQsMEVpFVksBeiNrNDBnlcRAxnIJspVO1FqhlrgIFgQAg1CAsIJKOjoBttnt0kisRCSZjlOkYjS8PTGczWezLqoAERgWXCXmpHlgnrCEVAA

x4rf41
  • 5,184
  • 2
  • 22
  • 33
  • This is very interesting! Thank you for teaching me something I didn't know of before =). I did end up using your method. The `this.constructor as any` statement seems very ugly, and I can pass any type or number of arguments to the constructor this way. However, it seems to encapsulate these problems in the base class only and the rest of the classes become more elegant. Thank you! – AweSIM Nov 14 '20 at 21:00