Alex Craft
Alex Craft

Reputation: 15336

TypeScript polymorphic return type

Let's say we want to implement data types hierarchy, what type should be used?

interface Collection<T> {
  map(f: (v: T) => T): ???
}

interface Array<T> extends Collection<T> {
  map(f: (v: T) => T): ??? { return new Array<T>() }
}

interface LinkedList<T> extends Collection<T> {
  map(f: (v: T) => T): ??? { return new LinkedList<T>() }
}

Polymorhping this is not working

interface Collection<T> {
  map(f: (v: T) => T): this
}

interface Array<T> extends Collection<T> {
  map(f: (v: T) => T): this { return new Array<T> }
  // ^ error `Error: type Array<T> is not assignable to type this`
}

Upvotes: 2

Views: 1459

Answers (2)

Karol Majewski
Karol Majewski

Reputation: 25790

If I understand correctly, what you're looking for is f-bounded polymorphism (“overloading this”).

interface Collection<T> {
  map(this: Collection<T>, f: (v: T) => T): this;
}

The type of this is always used as the first parameter. Notice we can add more overloads. For example, to transform a Collection<number> into a Collection<string>, we could do:

interface Collection<T> {
  map(this: Collection<T>, f: (v: T) => T): this;
  map<U>(this: Collection<T>, f: (v: T) => U): Collection<U>;
}

Usage:

declare const collection: Collection<number>;

collection.map(x => x.toString())

Upvotes: 1

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249636

The honest use of polymorphic this is when you are actually returning this. Consider the following classes

export interface Collection<T> {
  map(f: (v: T) => T): this
}

class Array<T> implements Collection<T> {
  map(f: (v: T) => T): this { return new Array<T>() as this; }
}

class MyArray<T> extends Array<T>{ }

new MyArray().map(o=> true) instanceof MyArray // false, but map returns MyArray

Nothing forces MyArray to override map and return a correct instance, so when calling map on MyArray we still get an instance of Array (somewhat surprisingly I might say).

That being said there is not good way to model this in the type system, that is a class that when derived must implement a specific method without the class being abstract.

You could use polymorphic this and use the constructor property to protect against the above scenario, but there is no way to constrain the derived constructor to have no parameters (as the call below would require). So this solution is not fully type safe, and does use some type assertion to get the job done but I think it is as safe as it gets:

 interface Collection<T> {
  map(f: (v: T) => T): this
}

class Array<T> implements Collection<T> {
  map(f: (v: T) => T): this { return new (this.constructor as any)(); }
}

class MyArray<T> extends Array<T>{ }

console.log(new MyArray().map(o=> true) instanceof MyArray) // ture

Upvotes: 1

Related Questions