I'm using typescript in a project which uses BackboneJS and in a certain case, a method overload gets lost.
I narrowed it down to this case:
class Model {
set<A extends string>(key: A, value: any): this;
set(attrs: Record<string, any>): this;
set<A extends string>(
keyOrAttrs: A | Record<string, any>,
value?: any
): this {
return this;
}
}
class ModelX extends Model {
getX(): number {
return 1;
}
getY(): number {
return 2;
}
}
export function testModel(model: Model) {
model.set("key", "value");
model.set({ key: "value" });
}
export function testModelX(model: ModelX) {
model.set("key", "value");
model.set({ key: "value" });
}
export function testModelModelX(model: Model | ModelX) {
model.set("key", "value");
model.set<string>("key", "value");
model.set<"key">("key", "value");
model.set({ key: "value" });
if ("getX" in model) {
// model is ModelX
model.set("key", "value");
model.getY();
} else {
// model is Model
model.set("key", "value");
}
}
The Model class resembles the Model from Backbone. It has a set method which can be used with key+value params or an object. This all works fine.
In the testModelModelX() function I can pass a Model or a ModelX, so within that function I can narrow the type by checking for "getX". That also works, but before the check, the type is Model | ModelX and here the set("key", "value") does not work, only the set({ key: "value" }):
test.ts:32:20 - error TS2554: Expected 1 arguments, but got 2.
32 model.set("key", "value");
~~~~~~~
test.ts:33:13 - error TS2558: Expected 0 type arguments, but got 1.
33 model.set<string>("key", "value");
~~~~~~
test.ts:34:13 - error TS2558: Expected 0 type arguments, but got 1.
34 model.set<"key">("key", "value");
~~~~~
It looks like the first method definition (set<A extends string>(key: A, value: any): this;) is lost in the Model | ModelX type. I was expecting both versions to work since the type for the set method is the same for Model and ModelX. Is there a reason the overload get's lost? I have tested with TS 5.6.3, 5.9.3 and nightly.
If the call signatures of Model.set() and ModelX.set() were actually identical, then it wouldn't be a problem to call the union of those call signatures. TypeScript has always been able to merge unions of identical function types and call them. But they're not really identical; they return the polymorphic this type, which is implicitly generic in the implementing class type. From the caller's perspective, your code looks more like this:
class Model {
set<A extends string>(key: A, value: any): Model;
set(attrs: object): Model;
set<A extends string>(keyOrAttrs: A | object, value?: any): this {
return this;
}
}
class ModelX extends Model {
getX(): number {
return 1;
}
getY(): number {
return 2;
}
set<A extends string>(key: A, value: any): ModelX;
set(attrs: object): ModelX;
set<A extends string>(keyOrAttrs: A | object, value?: any): this {
return this;
}
}
Here Model.set() returns Model and ModelX.set() returns ModelX. And thus the call signatures are not considered to be identical, so they're not merged... and you're still trying to call a union of methods.
TypeScript's support for calling unions of methods introduced in TypeScript 3.3, as implemented in microsoft/TypeScript#29011 only works if at most one of the union members is a generic function or an overloaded function. (Trying to unify generic and overloaded methods would be significantly more work for the compiler.) Since your methods are unfortunately generic and overloaded, and there are two of them (one for Model and one for ModelX), they can't be fully unified.
From my reading of the pull request diff it looks like TypeScript is able to unify both set(attrs: object): Model and set(attrs: object): ModelX into set(attrs: object): Model (since ModelX is a subtype of Model) so that part is callable. But the generic stuff is just lost.