Home > other >  Return new self equivalent in TypeScript for class inheritance
Return new self equivalent in TypeScript for class inheritance

Time:06-23

I have a base class Collection which provides basic array features. This class gets extended for other domain-specific use cases. When a "destructing" method like filter is called, it should return a new instance with the filtered elements (to continue making use of class methods instead of just getting the array back).

In PHP you'd use return new self() to return the actual child or parent class, based on what it was constructed on (I think for Java it's return obj.newInstance()). But with JS/TS I really struggle to find a solution to this. My current workaround is to overwrite the newInstance method by all child classes.

Is there a solution to this?

class Collection<E> {
  protected items: E[];

  constructor(items: any[] = []) {
    this.items = items;
  }

  // doesn't work, just one of many attempts
  protected newInstance(items: E[]) {
    return new Collection(items);

    //return new this.constructor(items); // "This expression is not constructable"

    // return new this.constructor.prototype(items); // another attempt, says "this.constructor.prototype is not a constructor"
  }

  size() {
    return this.items.length;
  }

  // should filter and return new instance to use class methods
  filter(callback: (item: any, index?: number) => boolean): this {
    // should be the actual instance (can be inherited)
    return this.newInstance(this.items.filter(callback)) as this;
  }
}

class NumberCollection extends Collection<number> {
  sum() {
    return this.items.reduce((a, b) => a   b, 0);
  }
}

let numbers = new NumberCollection([1, 2, 3, 4]);

console.log(numbers.sum()); // works,

// throws "sum() is not a function"
console.log(numbers.filter((n) => n > 1).sum());

CodePudding user response:

Sadly, this is one of those things that's easy in JavaScript and incredibly awkward in TypeScript.

In JavaScript, you'd do one of two things:

Unfortunately, though, you can't do that without type assertions or outright @ts-ignores in TypeScript. See this related question about the species pattern, to which the answer is: you can't do that.

I think your only realistic option, if filter (and such) always return an instance of the class they're called on (so it's #1 above, not #2), is to do what you've done with this as the return type annotation, using new this.constructor(/*...*/) with a @ts-ignore on it:

protected newInstance(items: E[]): this {
    // @ts-ignore - blech
    return new this.constructor(items);
}

Playground link

That correctly creates an instance of the class it's called on (even though TypeScript doesn't see the construction signature on this.constructor) because by default the prototype assigned to a class instance has a constructor property that refers to the constructor function: this.constructor in an instance created via new Collection is Collection; this.constructor in an instnace created via new NumberCollection is NumberCollection. (There are things you can do to mess that up, but class syntax makes it easy to avoid most of them.) So new this.constructor(/*...*/) creates a new object using (in the normal case) the constructor that was used to create this. As a result, you don't have that sum problem anymore; this works:

let numbers = new NumberCollection([1, 2, 3, 4]);
console.log(numbers.sum());
const x = numbers.filter((n) => n > 1);
//    ^? −− type is NumberCollection
console.log(x.sum()); // works

I don't like it, but as far as I know, we're stuck with it for now.

  • Related