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
Type Predicate Function Inconsistent Across Identical Inferred Types #49514
import{F}from"ts-toolbelt";// can easily be replaced with raw TS code if neededconstsatisfies=<TWide,>()=><TNarrowextendsTWide>(narrow: F.Narrow<TNarrow>)=>narrow;constisNotNull=<T,>(value: T|null): value is T=>value!==null;typeItem=Array<{value: string|null}>;constitem1=satisfies<Item>()([{value: "1"}]);constitem2=satisfies<Item>()([{value: "2"}]);constitem3=satisfies<Item>()([{value: null}]);constvalues1=[item1,item2,item3].flatMap(item=>item.map(p=>p.value));constfilteredValues1=values1.filter(isNotNull);// still contains `null` in type :(constvalues2: Array<"1"|"2"|null>=["1","2",null];// same reported type as `values`constfilteredValues2=values2.filter(isNotNull);// `null` correctly gets excluded from type
🙁 Actual behavior
filteredValues1 still contains null in its type when is shouldn't.
🙂 Expected behavior
filteredValues1 should have the same type as filteredValues2 (without null).
typeTry<A1extendsany,A2extendsany,Catch=never>=A1extendsA2 ? A1 : Catch;typeNarrowable=string|number|bigint|boolean;typeNarrowRaw<A>=(Aextends[] ? [] : never)|(AextendsNarrowable ? A : never)|({[KinkeyofA]: A[K]extendsFunction ? A[K] : NarrowRaw<A[K]>;});typeNarrow<Aextendsany>=Try<A,[],NarrowRaw<A>>;constsatisfies=<TWide,>()=><TNarrowextendsTWide>(narrow: Narrow<TNarrow>)=>narrow;/* ========================================================================== */constisNotNull=<T,>(value: T|null): value is T=>value!==null;typeItem=Array<{value: string|null}>;constitem1=satisfies<Item>()([{value: "1"}]);constitem2=satisfies<Item>()([{value: "2"}]);constitem3=satisfies<Item>()([{value: null}]);constvalues1=[item1,item2,item3].flatMap(item=>item.map(p=>p.value));constfilteredValues1=values1.filter(isNotNull);// still contains `null` in type :(constvalues2: Array<"1"|"2"|null>=["1","2",null];// same reported type as `values`constfilteredValues2=values2.filter(isNotNull);// `null` correctly gets excluded from type
Well, after a couple hours debugging, I know what’s causing this, but I’m not yet sure what to do about it. In the working values2 case, the "1" | "2" constituents of the union type are not “fresh literal” types. In values1, they are fresh, and so they get widened to string when we are instantiating the signature of isNotNull. (The inference candidate for its T is the union of literals, but at the last possible moment in getCovariantInference, it sees no reason not to widen.) Once we have inferred isNotNull<string>, we can no longer choose the type predicate overload for .filter because string is not a subtype of "1" | "2" | null, so the non-narrowing signature is chosen.
The obj3 example definitely doesn't seem right, so I think it makes sense to always infer regular (non-fresh) literal types for array literal elements and object literal properties.
Activity
RyanCavanaugh commentedon Jun 13, 2022
A repro with an inlined
Fwould be appreciated if anyone has timejoealden commentedon Jun 13, 2022
Ryan Cavanaugh (@RyanCavanaugh) here you go:
Playground link with relevant code
JoanVicens commentedon Jun 23, 2022
I'm having the same problem with the filter function filtering an array of any.
What seems odd is that the same line inside a function works fine.
Playground
andrewbranch commentedon Jul 8, 2022
Well, after a couple hours debugging, I know what’s causing this, but I’m not yet sure what to do about it. In the working
values2case, the"1" | "2"constituents of the union type are not “fresh literal” types. Invalues1, they are fresh, and so they get widened tostringwhen we are instantiating the signature ofisNotNull. (The inference candidate for itsTis the union of literals, but at the last possible moment ingetCovariantInference, it sees no reason not to widen.) Once we have inferredisNotNull<string>, we can no longer choose the type predicate overload for.filterbecausestringis not a subtype of"1" | "2" | null, so the non-narrowing signature is chosen.Literal freshness is not a system I’m super familiar with in the compiler. Can anyone advise on how this is expected to work? (Wesley Wigham (@weswigham) Anders Hejlsberg (@ahejlsberg)?)
ahejlsberg commentedon Jul 13, 2022
Here's a shorter example to illustrate the surprising effects of preserving literal freshness in array literal elements and object literal properties:
The
obj3example definitely doesn't seem right, so I think it makes sense to always infer regular (non-fresh) literal types for array literal elements and object literal properties.