Skip to content

Type argument inference issues for methods of Generic Class with constraints other than primary types #21734

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
lostfields opened this issue Feb 7, 2018 · 7 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Milestone

Comments

@lostfields
Copy link

lostfields commented Feb 7, 2018

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:

@lostfields lostfields changed the title Issues with Generic/Extends and IterableIterator? Issues with Generic/Extends and IterableIterator in Typescript 2.7.1? Feb 7, 2018
@lostfields lostfields changed the title Issues with Generic/Extends and IterableIterator in Typescript 2.7.1? Issues with generic/return type when extending class using generics in Typescript 2.7.1? Feb 7, 2018
@lostfields
Copy link
Author

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'.

@lostfields
Copy link
Author

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

@mhegazy mhegazy added Bug A bug in TypeScript Needs Investigation This issue needs a team member to investigate its status. labels Feb 13, 2018
@mhegazy mhegazy added this to the TypeScript 2.8 milestone Feb 13, 2018
@lostfields
Copy link
Author

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 lostfields changed the title Issues with generic/return type when extending class using generics in Typescript 2.7.1? Type argument inference issues for methods of Generic Class with constraints other than primary types Feb 13, 2018
@RyanCavanaugh
Copy link
Member

@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

@lostfields
Copy link
Author

lostfields commented Feb 13, 2018

@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

@mattmccutchen
Copy link
Contributor

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.

@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.4.0 milestone Feb 1, 2019
@RyanCavanaugh RyanCavanaugh removed Needs Investigation This issue needs a team member to investigate its status. labels Mar 7, 2019
@RyanCavanaugh RyanCavanaugh added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Bug A bug in TypeScript labels Mar 13, 2019
@RyanCavanaugh
Copy link
Member

#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...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

7 participants