Consider the following pseudo code:
abstract class X {}
class Y extends X {
static compare(a: Y, b: Y) {
return a.id - b.id;
}
}
class Z extends X {
static compare(a: Z, b: Z) {
return a.name.localCompare(b.name);
}
}
function sort<T extends X>(items: T[]) {
return items.sort(T.compare); // <-- illegal
}
Y.sort([new Y(), new Y()]); // <-- should use Ys compare
Z.sort([new Z(), new Z()]); // <-- should use Zs compare
The sort function should utilize the respective compare function of the type used to call it. This doesn't work because T can't be used like that. How can I change the generic function at the end so I can use the static compare function of Y?
(And before anyone comments, please do not suggest solutions how I could simplify this whole thing and avoid the issue altogether. Obviously, the above is an MCVE-esque example to illustrate the problem and not the full code I'm dealing with.)
Ideally you'd give your X
superclass a static
method that only allows it to operate on instances of the class. But that would require using a polymorphic this
type, and TypeScript currently doesn't support such types inside static
methods. See the longstanding open feature request at microsoft/TypeScript#5863. Until and unless that's ever implemented, we'll need to work around it.
One of the common workarounds is to make the method generic and give it a this
parameter that represents a similar constraint. So the static method will only be callable on a class of the right shape.
So let's say that we only want to be allowed to call sort()
on a class constructor which has a compare
method of the appropriate type. That type might be
interface Comparator<T> {
compare: (this: void, a: T, b: T) => number;
}
meaning that we require the class constructor to be a Comparator<T>
for some T
. (Note the void
this
context which says compare
must be callable without being bound to an object; that can be relaxed if we want, but hopefully that's good enough.) Then we'd write sort()
on X
like this:
abstract class X {
static sort<T extends X>(this: Comparator<T>, items: T[]) {
return items.sort(this.compare);
}
}
That compiles cleanly; let's test it out:
class Y extends X {
id: number = 0;
static compare(a: Y, b: Y) {
return a.id - b.id;
}
}
class Z extends X {
name: string = "";
static compare(a: Z, b: Z) {
return a.name.localeCompare(b.name);
}
}
Y.sort([new Y(), new Y()]); // okay
Z.sort([new Z(), new Z()]); // okay
Z.sort([new Y(), new Z()]); // error
// ---> ~~~~~~~ Y is not Z
X.sort([new Y(), new Z()]); // error
// <-- compare is missing from X
Looks good. Your desired calls are supported, and undesirable calls are rejected. You're only allowed to call Z.sort()
on an array of Z
instances, and you're not allowed to call X.sort()
directly at all because X
is not a Comparator
at all.