Skip to content

Generic type no more correctly inferred since 3.7 (now inferred as any). #36226

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
stephanedr opened this issue Jan 16, 2020 · 5 comments
Closed
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@stephanedr
Copy link

stephanedr commented Jan 16, 2020

TypeScript Version: 3.7.4, 3.8.0-dev.20200115

Search Terms: generic inferred any 3.7

Code

class FieldEvent<T> {
    constructor(readonly field: T) { }
}

class Field {
    listen<T extends FieldEvent<Field>>(type: new (...args: any[]) => T, listener: (event: T) => void): void { }
}

let field = new Field();
field.listen(FieldEvent, (event) => event.field);
// TS 3.7: event: FieldEvent<any>, so event.field: any
// Up to TS 3.6: event: FieldEvent<Field>, so event.field: Field

Expected behavior:
event inferred as FieldEvent<Field> (was the case up to TS 3.6).

Actual behavior:
event inferred as FieldEvent<any> since TS 3.7. Still the case with 3.8.0-dev.20200115.

Playground Link:
http://www.typescriptlang.org/play/index.html#code/MYGwhgzhAEBiCWBTEATAogN0QOwC4B4AVAPmgG8Aoaa6YAe2wlwCcBXYXO5gCmcTBQMQAT2gAzJKgBc0QgEpy0AL4UVFUJBgJkKclRoh4THEWiIAHrhwotk9Fjz5tqYsW65hAB0QzsiAO7Q3AB0oWDMAOYQMmDYwgDaALoKALykhAA00IbGfswy3IgOuDLy0GnQGHTwKHIyVTWKKmogiLjiduXQfoHOKNxyANwUEjrBOVbY3H2YOLhZhcWppEVzwaOoQxQA9NuyAMrQAMzBAOwyq3gyM8X4scLEWRB0ZsXrdjFxO3sAqp7QnAOx2CADYLsVrnZZo4+o9oM9XmsNihIToKEA
(Edit: playground link to official site, not beta).

Related Issues:

@RyanCavanaugh
Copy link
Member

This is the intended behavior. TypeScript has two candidates for T during inference:

  • The parameter position readonly field: T in the constructor (any)
  • The return type of the constructor function (FieldEvent<T>)

From the perspective of "how could listen possibly be implemented", we can tell that there's no way for listen to manufacture a value of type T via the return value, so the argument position must be higher priority, thus the any inference candidate is chosen.

Basically there's no way that listen could be implemented in a way that actually produces a FieldEvent<Field>, since the only place for this value to originate is through a constructor call that actually supplies an any. If instead you constrained that constructor call to a more specific type, you'd get the expected inference:

class FieldEvent<T> {
    constructor(readonly field: T) { }
}

class Field {
    listen<T extends FieldEvent<Field>>(type: new (...args: Field[]) => T, listener: (event: T) => void): void {
        // Plausible implementation
        const j = new type(this);
        listener(j);
    }
}

let field = new Field();
field.listen(FieldEvent, (event) => event.field);

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jan 23, 2020
@stephanedr
Copy link
Author

Thanks @RyanCavanaugh for the code, which indeed helped me to fix the issue.

However, playing with different versions of TS via playground:

  • event is inferred as FieldEvent<Field> from versions 2.7 to 3.6.
  • event is inferred as FieldEvent<any> only since version 3.7.

Is it an expected/intended change?

@RyanCavanaugh
Copy link
Member

Yes; when TS infers more-specific types from lower-priority sites, unsoundness results.

@typescript-bot
Copy link
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@stephanedr
Copy link
Author

@RyanCavanaugh, I wanted to apply your proposal to my code, but I cannot find the syntax working with overloads.
(let me know if you want I open a new ticket).

class FieldEvent<T extends Field> {
    constructor(readonly field: T) { }
}

class Field {
    listen<T extends FieldEvent<Field>>(type: new (field: this, ...args: any[]) => T, listener: (event: T) => void): void { }
}

class BeforeSetEvent<T extends number | string> extends FieldEvent<ValueField<T>> {
    constructor(field: ValueField<T>, public newValue: T) { super(field); }
}

class AfterSetEvent<T extends number | string> extends FieldEvent<ValueField<T>> {
    constructor(field: ValueField<T>, readonly oldValue: T) { super(field); }
}

class ValueField<T extends number | string> extends Field {
    constructor(public value: T) { super(); }

    listen<U extends BeforeSetEvent<T>>(type: new (field: this, ...args: any[]) => U, listener: (event: U) => void): void;
    listen<U extends AfterSetEvent<T>>(type: new (field: this, ...args: any[]) => U, listener: (event: U) => void): void;
    listen<U extends FieldEvent<Field>>(type: new (field: this, ...args: any[]) => U, listener: (event: U) => void): void;
    listen<U extends FieldEvent<Field>>(type: new (field: this, ...args: any[]) => U, listener: (event: U) => void): void {
        super.listen(type, listener);
    }
}

let field = new ValueField(0);
field.listen(FieldEvent, (event) => event.field);   // No overload matches this call.
field.listen(BeforeSetEvent, (event) => { event.field; event.newValue; });  // OK.
field.listen(AfterSetEvent, (event) => { event.field; event.oldValue; });  // No overload matches this call.

Playground

If we comment the overloads of ValueField.listen(), it compiles, but with event: BeforeSetEvent<any> and event: AfterSetEvent<any>.
So the reason of the overloads, to enforce T of the same type as ValueField.

It seems that TS only considers the 1st overload (not parsing all the overloads to find the 1st matching one).

Notes:

  • With TS 3.6, it is working without field: this - playground.
  • But this code fails in 3.7 (overload error).
  • With 3.6, adding field: this generates the same errors as with 3.7.

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

4 participants