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
Interaction of empty arrays and satisfies is unsatisfying #51853
List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily and help provide feedback.
✅ Viability Checklist
My suggestion meets these guidelines:
This wouldn't be a breaking change in existing TypeScript/JavaScript code
This wouldn't change the runtime behavior of existing JavaScript code
This could be implemented without emitting different JS based on the types of the expressions
This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
The functionality of satisfies, as implemented in #46827, was discussed in #7481 and then #47920, and it was decided that the "safe upcast" functionality couldn't really be directly supported while also supporting all the other use cases. In this comment it was shown that to get a safe upcast you could write x satisfies T as T which, while annoying, is at least possible:
I think it would be wonderful to have a specific operator for safe upcasting (shorten x satisfies T as T to x satisfiesas T / x satisfas T / x sassafras T / x sass T / x ass T), like an in-line type annotation. (Is there an open issue for this?) But I don't think satisfies alone will ever do this for you.
(Note: for empty arrays in particular it would lovely if nothing were ever inferred as never[], but I don't know that they'd want to special case how empty arrays behave with satisfies.)
Reacted by Martin Johns, liu.t, Noah, Esa-Matti Suuronen, Darryl Noakes, Brandon Bennett, ryan-0324 and Matt Kantor
(Note: for empty arrays in particular it would lovely if nothing were ever inferred as never[], but I don't know that they'd want to special case how empty arrays behave with satisfies.)
I believe Ryan Cavanaugh (@RyanCavanaugh) is on record as saying that nothing should ever be inferred as never[]but I can't find the relevant comment at the moment.
changed the title [-]update infer type when use `satisfies` keyword[/-][+]Interaction of empty arrays and `satisfies` is unsatisfying[/+]on Dec 12, 2022
Some notes from discussion and post-discussion thinking
"Copying" the contextual type is just wrong (the first revision of the PR didn't do this either) because you might have const m = [] satisfies MyCustomArrayType, which should fail. You need to get the element type of the array and construct an array type from it.
Which element types, though? You might write
constm=[]satisfiesArrayLike<number>
where the desired type is presumably m: number[]
For unions, things get tricky. It's tempting to say that for an offered type T[] | U[], we should produce either T[] | U[] or (T | U)[], but neither of these are right. Counterexamples:
// Should pass, but would fail if [] has type (string | number)[]constm: string[]|number[]=[]satisfiesstring[]|number[]
typeObj={a: string[]}|{a: number[]};// Should pass, but would fail if [] has type string[] | number[]constm: Obj={a: []}satisfiesObj;
These examples are illustrative -- the only thing that works here is to give the [] type never[] (this is what it has "in reality" today), where never is specifically derived from (T & U)[]
I'll update the PR to implement the behavior that we give an empty array a type of the intersection of its contextual types' element types if they are ArrayLike
Why is the default inference for [] (in the absence of a contextual type) not unknown[]? It feels weird to infer never[] since anyone initializing a variable with an empty array (or returning one from a function without a return type annotation) likely intends to push stuff into it afterwards. Is it just to encourage writing explicit type annotations in this case?
Why is the default inference for [] (in the absence of a conditional type) not unknown[]? It feels weird to infer never[] ...
Good question and a lot to unpack here. I'll speak only in terms of today's behavior for clarity. Empty arrays are (again, currently) not affected by contextual typing except that they can form 0-length tuples in the presence of a tuple-like contextual type. So if you look at a call like
Here the expression [] has type never[] and the call succeeds. Had [] had type unknown[], the call would have failed.
For types given to inferred variables, the situation is much tricker due to evolving arrays. But object properties don't qualify for evolving arrays, so the situation is analogous:
This call is processed the same way: obj.a is never[]. This one is probably the most important to get right since there is no contextual typing to "fix" the type of [] in this example.
likely intends to push stuff into it afterwards
Sometimes yes, sometimes no. There are a lot of APIs like
sendEmail(subject,body,/*attachments*/[]);
where it makes sense to just give someone a fresh empty array literal that doesn't have any bindings pointing to it. The "sometimes yes" case is indeed why we went to the trouble to make evolving arrays (those whose type is inferred via CFA looking for calls to push/etc), since in the case where you start with a variable initialized with [], almost certainly it's not going to stay empty.
TL;DR handling both covariant and contravariant inference is hard
Reacted by Bruce Pascoe, Matt Kantor, Toni Villena, 吖猩, liu.t, Darryl Noakes and Ricardo Fernández Serrata
Ryan Cavanaugh (@RyanCavanaugh) I hit upon this issue yesterday in a case where I want to access an array property of an object where only some of the possible properties are defined:
typeExample={a?: string[]|null;aa?: string[]|null;}constexample={a: [],}satisfiesExample;const{ a }=example;// Argument of type 'string' is not assignable to parameter of type 'never'.(2345)a.push('');typeNonNullableFields<T>={[PinkeyofT]: NonNullable<T[P]>;};constworkaround: NonNullableFields<Required<Example>>={a: [],aa: [],}const{a: b}=workaround;b.push('');
The safe upcast pattern doesn't work here, only workaround I found was to define all the properties and make a NonNullableFields<Required<Example>> annotation on the variable, this requires unnecessary arrays to be defined. Is there any better workarounds for this pattern?
It's a tangent issue but I don't think it's worth a new issue:
// Type 'never[]' is not assignable to type 'unknown[] & { [x: string]: unknown; }'.// Type 'never[]' is not assignable to type '{ [x: string]: unknown; }'.// Index signature for type 'string' is missing in type 'never[]'.(2322)constresult: unknown[]&{[x: string]: unknown;}=[];
Activity
jcalz commentedon Dec 11, 2022
The functionality of
satisfies, as implemented in #46827, was discussed in #7481 and then #47920, and it was decided that the "safe upcast" functionality couldn't really be directly supported while also supporting all the other use cases. In this comment it was shown that to get a safe upcast you could writex satisfies T as Twhich, while annoying, is at least possible:But if you're already assigning to a variable you might as well annotate:
Or, depending on what you're trying to do:
I think it would be wonderful to have a specific operator for safe upcasting (shorten
x satisfies T as Ttox satisfiesas T/x satisfas T/x sassafras T/x sass T/x ass T), like an in-line type annotation. (Is there an open issue for this?) But I don't thinksatisfiesalone will ever do this for you.(Note: for empty arrays in particular it would lovely if nothing were ever inferred as
never[], but I don't know that they'd want to special case how empty arrays behave withsatisfies.)fatcerberus commentedon Dec 11, 2022
I believe Ryan Cavanaugh (@RyanCavanaugh) is on record as saying that nothing should ever be inferred as
never[]but I can't find the relevant comment at the moment.edit: Found it. #47531 (comment)
RyanCavanaugh commentedon Dec 12, 2022
It's probably time to revive #47898
[-]update infer type when use `satisfies` keyword[/-][+]Interaction of empty arrays and `satisfies` is unsatisfying[/+]RyanCavanaugh commentedon Dec 13, 2022
Some notes from discussion and post-discussion thinking
"Copying" the contextual type is just wrong (the first revision of the PR didn't do this either) because you might have
const m = [] satisfies MyCustomArrayType, which should fail. You need to get the element type of the array and construct an array type from it.Which element types, though? You might write
where the desired type is presumably
m: number[]For unions, things get tricky. It's tempting to say that for an offered type
T[] | U[], we should produce eitherT[] | U[]or(T | U)[], but neither of these are right. Counterexamples:These examples are illustrative -- the only thing that works here is to give the
[]typenever[](this is what it has "in reality" today), whereneveris specifically derived from(T & U)[]I'll update the PR to implement the behavior that we give an empty array a type of the intersection of its contextual types' element types if they are ArrayLike
fatcerberus commentedon Dec 13, 2022
Why is the default inference for
[](in the absence of a contextual type) notunknown[]? It feels weird to infernever[]since anyone initializing a variable with an empty array (or returning one from a function without a return type annotation) likely intends to push stuff into it afterwards. Is it just to encourage writing explicit type annotations in this case?RyanCavanaugh commentedon Dec 13, 2022
Good question and a lot to unpack here. I'll speak only in terms of today's behavior for clarity. Empty arrays are (again, currently) not affected by contextual typing except that they can form 0-length tuples in the presence of a tuple-like contextual type. So if you look at a call like
Here the expression
[]has typenever[]and the call succeeds. Had[]had typeunknown[], the call would have failed.For types given to inferred variables, the situation is much tricker due to evolving arrays. But object properties don't qualify for evolving arrays, so the situation is analogous:
This call is processed the same way:
obj.aisnever[]. This one is probably the most important to get right since there is no contextual typing to "fix" the type of[]in this example.Sometimes yes, sometimes no. There are a lot of APIs like
where it makes sense to just give someone a fresh empty array literal that doesn't have any bindings pointing to it. The "sometimes yes" case is indeed why we went to the trouble to make evolving arrays (those whose type is inferred via CFA looking for calls to
push/etc), since in the case where you start with a variable initialized with[], almost certainly it's not going to stay empty.TL;DR handling both covariant and contravariant inference is hard
msand commentedon May 9, 2023
Ryan Cavanaugh (@RyanCavanaugh) I hit upon this issue yesterday in a case where I want to access an array property of an object where only some of the possible properties are defined:
https://www.typescriptlang.org/play?#code/C4TwDgpgBAogHgQwLZgDbQLxQN4CgoFQID8AXFAM7ABOAlgHYDmA2gLpQA+U9ArqqgG58hBCXJU6TNp259BuAL65cAYwD29KlAiIU6KFjyEi5NgBpFlBMFoUAZrQgVYutBCGqNW7ESgKD2q7oHggAdGA8FAAWABQA5HEAlB6gkFAAchrpcggARugAYo6oACYUADwAKgB8AUaEzAAKUAxQANYQIGp2UJWs5Jn02fx56FVNrNVCCh7qmsBQAO5q1G0I1Go89CUDWTn5EEUQpRUAShAAjjy01BAl5fDIbtW1hsIECKasFsaiXxZKTzzHAmKC5PwBZardabbYeXLhSKxBLJIA
The safe upcast pattern doesn't work here, only workaround I found was to define all the properties and make a
NonNullableFields<Required<Example>>annotation on the variable, this requires unnecessary arrays to be defined. Is there any better workarounds for this pattern?Andarist commentedon Apr 10, 2025
It's a tangent issue but I don't think it's worth a new issue: