Typescript: Type argument inference issues for methods of Generic Class with constraints other than primary types

Created on 7 Feb 2018  ·  7Comments  ·  Source: microsoft/TypeScript

I have a strange one that I can't figure out. It works fine with Typescript before 2.7.* and it has to be some kind of related to the abstract method with generics. When I remove that abstract method, it works. It's also working if I replace the abstract generic with any, from public abstract evaluate(items: Array<TEntity>): Array<TEntity> to public abstract evaluate(items: Array<any>): Array<any>

I have no idea what to name this issue, sorry for the title.


TypeScript Version: 2.7.1

Code

enum OperatorType {
    Take        = 1 << 1,
    Skip        = 1 << 2,
}

class Operations<TEntity> {
    public first<T extends Operator<TEntity>>(operator: { new (...args: any[]): T }): T {    
        return null;
    }
}

abstract class Operator<TEntity> {
    constructor(public type: OperatorType) {
    }

    public abstract evaluate(items: Array<TEntity>): Array<TEntity>
}

class SkipOperator<TEntity> extends Operator<TEntity> {
    constructor(public count: number) {
        super(OperatorType.Skip);
    }

    public evaluate(items: Array<TEntity>): Array<TEntity> {
        return null;
    } 
}

interface ICar {
    id: number
}

let count_failing = new Operations<ICar>().first(SkipOperator).count;
let count_working = new Operations<{}>().first(SkipOperator).count;

Expected behavior:
compile without any error

Actual behavior:
throws an error; error TS2339: Property 'count' does not exist on type 'Operator<ICar>'. even when the signature of method first indicates it should return SkipOperator instead of Operator.

Related Issues:

Working as Intended

All 7 comments

I'm trying different solutions to solve my issue, and I came over that extending classes and override methods with generic constraints doesn't work well.

let func: (<T extends Array<number>>(items: T) => T) = (items: Array<number>) => {
    return new Array<number>();
}

The error I get is

error TS2322: Type '(items: number[]) => number[]' is not assignable to type '<T extends number[]>(items: T) => T'.
  Type 'number[]' is not assignable to type 'T'.

If I change my code in first post and remove the generics and adds an interface instead it works as intended

interface IOperator<TEntity> {
    evaluate(items: Array<TEntity>): Array<TEntity>
}

abstract class Operator<TEntity> implements IOperator<TEntity> {
    constructor() {
    }

    public evaluate(items: any): any {
        return null;
    }
}

class SkipOperator<TEntity> extends Operator<TEntity> {
    constructor(public count: number) {
        super();
    }

    public evaluate(items: Array<TEntity>): Array<TEntity> {
        return null;
    }
}

But, as I mentioned in second post, it seems like constraints and type declarations isn't inherited since this doesn't work:

let skip: SkipOperator<ICar> = new Operations<ICar>().first(SkipOperator);

The error returned is

error TS2322: Type 'SkipOperator<{}>' is not assignable to type 'SkipOperator<ICar>'.
  Type '{}' is not assignable to type 'ICar'.
    Property 'id' is missing in type '{}'.

works fine in Typescript < 2.7

This example is better to understand where the problem really is - it has something to do with "type argument inference" that isn't solved correctly for child methods of a generic class with constraints other than primary types.

This work as it should and throws an error because of the generic constraint

let first = <T extends Number>(items: Array<T>): T => {
    return items.shift();
}

let a = first([1,2,3]);
let b = first(["a","b","c"]); // error TS2345: Argument of type 'string[]' is not assignable to parameter of type 'Number[]'

This also works as it should, it inherits TType from the generic class

class Item<TType> {
    public first<T extends Array<TType>>(items: T) {
        return items.shift();
    }
}

let c = new Item<number>().first([1,2,3]);
let d = new Item<number>().first(["a", "b", "c"]); //error TS2345: Argument of type 'string[]' is not assignable to parameter of type 'number[]'

But, whenever I use more complex structures as constraint it never fails

class Car {
}

let e = new Item<Car>().first([new Car(), new Car(), new Car()]);
let f = new Item<Car>().first([1,2,3]); // no error because signature of first is first<number[]> instead of first<Car[]>

The issue here is that the signature for e line is Item<Car>.first<Car[]>and for f line is Item<Car>.first<number[]>, the type TType is never assigned down to method first

@lostfields the issue is that you have an empty class (see the FAQ). Adding a property or method to the class shows an error as you would expect

@RyanCavanaugh great, then my last example works. So the issue is something with type argument inference with constructor arguments then? because this fails too:

interface ICar {
    id: number
}

interface IOperator<TEntity> {
    valueOf(): TEntity 
}

class SkipOperator<TEntity> implements IOperator<TEntity> {
    constructor(public count: number = 5) {        
    }

    public valueOf(): TEntity {
        return null;
    }
}

class Collection<TEntity> {
    public first<T extends IOperator<TEntity>>(operator: new () => T): T {    
        return null;
    }
}

let count: number = new Collection<ICar>().first(SkipOperator).count

Throws error TS2339: Property 'count' does not exist on type 'IOperator<ICar>' when it should return SkipOperator<ICar> ?

If I change valueOf(): TEntity to valueOf(): number in IOperator/SkipOperator it works as intended. Both examples works in typescript 2.6.2

I was bored, so I looked into this. The behavior changed in #19345.

As far as I understand, TypeScript is matching up these two construct signatures:

SkipOperator:

new <TEntity>(count?: number): SkipOperator<TEntity>

Parameter of first:

new(): T

Before #19345, the SkipOperator signature would be erased to:

new (count?: number): SkipOperator<any>

Then T gets inferred as SkipOperator<any>, which works .

After #19345, the SkipOperator signature is replaced by its base signature with the type parameter TEntity replaced by its constraint {}:

new (count?: number): SkipOperator<{}>

Then an inference of SkipOperator<{}> is made for T, but since it doesn't satisfy the constraint IOperator<ICar>, T is set to the constraint instead.

19345 was very much intentional. I'd say the recommended thing would be to write

    public first<T extends { new(): IOperator<TEntity> }>(operator: T): InstanceType<T> {    

which makes the sample work without error (though does go against other guidance we write...)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

fwanicka picture fwanicka  ·  3Comments

kyasbal-1994 picture kyasbal-1994  ·  3Comments

seanzer picture seanzer  ·  3Comments

jbondc picture jbondc  ·  3Comments

bgrieder picture bgrieder  ·  3Comments