You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Generic type no more correctly inferred since 3.7 (now inferred as any). #36226
classFieldEvent<T>{constructor(readonlyfield: T){}}classField{listen<TextendsFieldEvent<Field>>(type: new(...args: any[])=>T,listener: (event: T)=>void): void{}}letfield=newField();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.
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:
Ryan Cavanaugh (@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.
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.
Activity
RyanCavanaugh commentedon Jan 23, 2020
This is the intended behavior. TypeScript has two candidates for
Tduring inference:readonly field: Tin the constructor (any)FieldEvent<T>)From the perspective of "how could
listenpossibly be implemented", we can tell that there's no way forlistento manufacture a value of typeTvia the return value, so the argument position must be higher priority, thus theanyinference candidate is chosen.Basically there's no way that
listencould be implemented in a way that actually produces aFieldEvent<Field>, since the only place for this value to originate is through a constructor call that actually supplies anany. If instead you constrained that constructor call to a more specific type, you'd get the expected inference:stephanedr commentedon Jan 24, 2020
Thanks Ryan Cavanaugh (@RyanCavanaugh) for the code, which indeed helped me to fix the issue.
However, playing with different versions of TS via playground:
eventis inferred asFieldEvent<Field>from versions 2.7 to 3.6.eventis inferred asFieldEvent<any>only since version 3.7.Is it an expected/intended change?
RyanCavanaugh commentedon Jan 24, 2020
Yes; when TS infers more-specific types from lower-priority sites, unsoundness results.
typescript-bot commentedon Jan 26, 2020
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
stephanedr commentedon Jan 30, 2020
Ryan Cavanaugh (@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).
Playground
If we comment the overloads of
ValueField.listen(), it compiles, but withevent: BeforeSetEvent<any>andevent: AfterSetEvent<any>.So the reason of the overloads, to enforce
Tof the same type asValueField.It seems that TS only considers the 1st overload (not parsing all the overloads to find the 1st matching one).
Notes:
field: this- playground.field: thisgenerates the same errors as with 3.7.