I'm going to add some properties to your code to illustrate this.
interface Base {
bar: string;
}
interface Child extends Base {
magic: number;
}
class Foo<T extends Base> {
constructor(public callback: (data: T) => void) {
// '{ bar: string; }' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'Base'.
callback({ bar: "sweet" });
}
}
let map: Map<number, Foo<Base>> = new Map();
rpcCall((d: Child) => d.magic);
function rpcCall<T extends Base>(callback: (data: T) => void) {
// 'Base' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'Base'
map.set(0, new Foo(callback));
}
Oh dear. More errors!
As you can see, inside Foo
I attempted to call the callback you defined, and passed it an object that extends Base
, but it threw an error back at me.
What if the callback is expecting a Child
? Sure, anything that extends Base
is fine as far as Foo
cares, but how do you know what the callback itself is expecting?
If you take a look at my usage of rpcCall
where I legally gave it a callback expecting a Child
, I am trying to use the magic
property (which is marked as required in my Child extends Base
interface).
Basically at some point there might be an attempt to use something that does not exist on Base
.
If you replaced the generics with simply Base
would make some of the errors would go away, but doing something like rpcCall((d: Child) => d.magic)
would be disallowed. If you don't need non-base properties in these areas, this might be ok for you.
The second version you provided works because Foo2
is an empty class (the generic is completely ignored, in fact, since you don't use it).
An empty class is equivalent to {}
, which basically accepts everything except null
and undefined
(as far as I am aware). And when it comes to union types, any "looser" parameter will take precedence over the more stringent ones.
The below are all equivalent (in this case):
Map<number, Foo<Base> | Foo2<Base>>
Map<number, Foo<Base> | {}>
Map<number, Foo<Foo2<Base>>
Map<number, Foo<{}>
In fact, if you piped | any
to the end of any union, that union effectively becomes any
.
class BaseWithRaisins extends Base { raisins = 100; } rpcCall((x: BaseWithRaisins) => console.log(x.raisins.toFixed())); map.get(0)!.callback(new Base());
? That will throw no compile-time errors, but you will explode at runtime, since aFoo<Base>
's callback must accept anyBase
, but you gave it something that will only handle aBaseWithRaisins
. – jcalzmap
be aMap<number, Foo<any>>
, but I don't see what you'll actually be able to do with it. Presumably your use case somewhere involves pulling things out of themap
and calling the callbacks, at which point you'll be stuck not knowing what types of argument it accepts. Could you provide a fuller use case with example code for how you expect to use the map? – jcalz