Jawensi
Jawensi

Reputation: 25

Type of an Array of interface depending of T

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

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

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 

Playground Link

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

Playground Link

Upvotes: 1

Related Questions