Reputation: 25
I have an Interface that has a function depending of T which can extends form a string or a number.
interface IConstructorReg<T extends string | number> {
constructor: new (...args: any[]) => BaseImportObj;
check: (data: T, fileName: string) => boolean;
}
I want to push objects that implements that interface.
function checkFun1(data: string, fileName: string) {
// Severals Checkers ...
return true;
}
const objImp1: IConstructorReg<string> = { constructor: ASCReader, check: checkFun1 };
function checkFun2(data: number, fileName: string) {
// Severals Checkers ...
return true;
}
const objImp2: IConstructorReg<number> = { constructor: ASCReader, check: checkFun2 };
const b: Array<IConstructorReg<string | number>> = [];
b.push(objImp1); // Type 'string | number' is not assignable to type 'string'.
b.push(objImp2); // Type 'string | number' is not assignable to type 'number'.
. Which is the correct type of the Array "b"?
Upvotes: 0
Views: 38
Reputation: 250366
At it's core this is an issue of variance, namely the variance of IConstructorReg
. Since T
appears as the parameter in a function signature, IConstructorReg
is contravariant in T
. This means that IConstructorReg<string | number>
is not a base type for IConstructorReg<string>
, but rather the other way around (the contra in contravariant comes from the arrow of inheritance pointing in the opposite direction as for T
, you can read more about variance in this answer)
To get this to work, you can do one of two this.
You can change Array<IConstructorReg<string | number>>
to Array<IConstructorReg<string> | IConstructorReg<number>>
this will mean that the array can contain either IConstructorReg<number>
or a IConstructorReg<string>
. But when you use items from the array you need to check which one of the two it is in order to be able to call check
, and at the moment your interface does not have a way to preform this check at runtime:
const b: Array<IConstructorReg<string> | IConstructorReg<number>> = [];
b.push(objImp1);
b.push(objImp2);
b[0].check(0, "") // Argument of type 'number' is not assignable to parameter of type 'never'.
(b[0] as IConstructorReg<number>).check(0, "") // type assertion is an option
The other solution is to use method syntax instead of function field syntax to define check
. This will make IConstructorReg
bivariant in T
. While this will not require a type assertion or a check, it is inherently type unsafe as either string
or number
will be passable to an object that expects one or the other:
interface IConstructorReg<T extends string | number> {
constructor: new (...args: any[]) => BaseImportObj;
check(data: T, fileName: string): boolean;
}
const b: Array<IConstructorReg<string | number>> = [];
b.push(objImp1);
b.push(objImp2);
b[0].check(0, ""); // Ok
b[0].check("0", ""); // Also Ok
Upvotes: 1