Reputation: 16657
I have a problem with using a generic in a class. There's a class Item
that accepts a generic T
that extends constraining ItemValue
type and there's a type Items
that contains instances of Item
typed as Item<ItemValue
>.
The problem is that the compiler does not pass a potentially valid generic argument through ItemValue
constraint because callback
property of Item
is using it as a callback value.
If I removed the callback
property from the class, there would be no error.
Further, I tried to replicate this without class, and there's no such error. Do you think it's a bug, or am I doing something wrong?
type ItemValue = number | string;
type Callback<T> = (value: T) => void;
class Item<T extends ItemValue> {
private value: T;
// If you comment this out, there will be no error
private callback: Callback<T> = () => { }
constructor(value: T) {
this.value = value;
}
}
type Items = {
[index: string]: Item<ItemValue>
};
let items: Items = {
a: new Item<string>('') // error
}
// Type 'Item<string>' is not assignable to type 'Item<ItemValue>'.
// Types of property 'callback' are incompatible.
// Type 'Callback<string>' is not assignable to type 'Callback<ItemValue>'.
// Type 'ItemValue' is not assignable to type 'string'.
// Type 'number' is not assignable to type 'string'.
// This will also result in a similar error
// type AnItem = Item<ItemValue>;
// let a: AnItem = new Item<string>('');
// Here trying to do the same without a class.
function addCallback<T extends ItemValue>(cb: Callback<T>) {
}
function prepareAddCallback(cb: Callback<string>) {
// No problems with putting Callback<string> to Callback<ItemValue>
// Is it a bug?
addCallback(cb);
}
As @Joey pointed out in his answer:
string
is assignable tostring | number
, but(val: string) => void
is not assignable to(val: string | number) => void
type CallbackStrOrNum = (value: string | number) => void;
const addCallback = (cb: CallbackStrOrNum) => { };
addCallback((value: string) => { }); // error
let x: string | number = ''; // ok
This narrows down the issue. My question now would be, why is that?
To be clear, I solved the issue in this example by defining private callback: Callback<ItemValue> = () => { }
instead of private callback: Callback<T> = () => { }
Having the generic in the callback parameter was not critical here. The lesson here is to not define function types properties that have a generic, as then the class will be inextensible, e.g. Item<number>
would not be assignable to Item<string | number>
Also, in the real example, it was a dictionary of callbacks, in case you are wondering why I didn't define callback
as a regular method.
Upvotes: 2
Views: 650
Reputation: 1602
Note that string
is assignable to string | number
, but (val: string) => void
is not assignable to (val: string | number) => void
.
In your example, Item<string>
is not actually assignable to Item<string | number>
because the callback
properties don't have compatible types (i.e. (value: string) => void
is not assignable to (value: string | number) => void
).
Edit:
To be clear, this is by design and is a natural consequence of the type union operator (|
). Otherwise, the following code would compile, which would certainly be an issue.
const callback: (value: string | number) => void = (str: string) => {str.toLowerCase()};
callback(4);
Upvotes: 1
Reputation: 409
I changed this and it removed the error:
let items: Items = {
a: new Item<ItemValue>('') // (No) error
}
I believe it's because ItemValue can be a string OR a number so Item< string> wouldn't specific enough. It would also work with:
let items: Items = {
a: new Item<string | number>('') // (No) error
}
Upvotes: 0