Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Confusing error with generic container in Typescript

Tags:

typescript

Consider this code:

class Base {}

class Foo<T extends Base> {
  constructor(public callback: (data: T) => void) {
  }
}

let map: Map<number, Foo<Base>> = new Map();

function rpcCall<T extends Base>(
  callback: (data: T) => void,
): void {
  map.set(0, new Foo<T>(callback));
}

It gives me this error:

Argument of type Foo<T> is not assignable to parameter of type Foo<Base>.

Type Base is not assignable to type T.

Base is assignable to the constraint of type T, but T could be instantiated with a different subtype of constraint Base.

I can't see why this shouldn't work. The error message seems correct, but I don't see why that is an error. I want T to be allowed to be a different subtype of constraint Base.

Also, this does work:

class Base { }

class Foo<T extends Base> {
  constructor(public callback: (data: T) => void) {
  }
}

class Foo2<T extends Base> {
}

let map: Map<number, Foo<Base> | Foo2<Base>> = new Map();

function rpcCall<T extends Base>(
  callback: (data: T) => void,

): void {
  map.set(0, new Foo<T>(callback));
}
like image 409
Timmmm Avatar asked May 19 '26 02:05

Timmmm


1 Answers

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.

like image 185
ed' Avatar answered May 21 '26 07:05

ed'



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!