-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Fix destructuring and narrowing interaction #47337
Conversation
src/compiler/checker.ts
Outdated
function getTypeForVariableLikeDeclaration(declaration: ParameterDeclaration | PropertyDeclaration | PropertySignature | VariableDeclaration | BindingElement | JSDocPropertyLikeTag, includeOptionality: boolean): Type | undefined { | ||
function getTypeForVariableLikeDeclaration( | ||
declaration: ParameterDeclaration | PropertyDeclaration | PropertySignature | VariableDeclaration | BindingElement | JSDocPropertyLikeTag, | ||
includeOptionality: boolean, // >> TODO: change this to use a flag in check mode; figure out how this interacts with caching |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we decide to move forward with this PR, I'm going to replace the use of includeOptionality
parameter by a new check mode flag.
function func2<T extends Z>(value: T) { | ||
if (value.kind === "f") { | ||
const { f: f1 } = value; | ||
const { f: { a, ...spread } } = value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this case is any more weird than this whole thing already is, but... ...spread
here is not going to have a generic type, since we first get the non-generic type of value
, then find out the non-generic type of property f
on that type, and that's the type we use to compute the type of spread
.
const { kind, ...r1 } = t; | ||
const r2 = (({ kind, ...rest }) => rest)(t); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's an inconsistency that was already present. We have:
r1 : Omit<T, "kind">
, but r2 : { a: string; }
.
@typescript-bot perf test this |
@typescript-bot perf test this |
Heya @orta, I've started to run the perf test suite on this PR at 23c38be. You can monitor the build here. Update: The results are in! |
@orta Here they are:Comparison Report - main..47337
System
Hosts
Scenarios
Developer Information: |
@typescript-bot pack this |
Hey @gabritto, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build and an npm module you can use via |
RestBindingElement = 1 << 5, // Checking a type that is going to be used to determine the type of a rest binding element | ||
// e.g. in `const { a, ...rest } = foo`, when checking the type of `foo` to determine the type of `rest`, | ||
// we need to preserve generic types instead of substituting them for constraints | ||
IncludeOptionality = 1 << 6, // >> TODO: description, replace bool param with flag |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add this flag instead of the includeOptionality
parameter in another commit, once we settle on the other changes of this PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should definitely get this change in the PR as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since replacing the includeOptionality
by a CheckMode
flag affects our caching behavior in checkExpressionCached
(or we'll have to add an exception for that flag only), I'm gonna merge this without that part for the 4.6 RC, and I can add this change in another PR if we really want this.
@ahejlsberg any concerns? 4.6 RC is fast approaching and we'd like to have this in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good in general, but would like to see a few changes first.
RestBindingElement = 1 << 5, // Checking a type that is going to be used to determine the type of a rest binding element | ||
// e.g. in `const { a, ...rest } = foo`, when checking the type of `foo` to determine the type of `rest`, | ||
// we need to preserve generic types instead of substituting them for constraints | ||
IncludeOptionality = 1 << 6, // >> TODO: description, replace bool param with flag |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should definitely get this change in the PR as well.
const symbol = getSymbolOfNode(node); | ||
return symbol && getSymbolLinks(symbol).type || getTypeForVariableLikeDeclaration(node, /*includeOptionality*/ false); | ||
return symbol && getSymbolLinks(symbol).type || getTypeForVariableLikeDeclaration(node, /*includeOptionality*/ false, checkMode); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we should definitely fold the includeOptionality
into CheckMode
.
Fixes #45762
Background
In 4.3, we improved control-flow narrowing of generic types (#43183). When we are about to go into control-flow narrowing, we call a function
getNarrowableTypeForReference
, that returns the type we are going to use in narrowing.As of 4.3, when we call
getNarrowableTypeForReference
on something of typeT extends C
, we often get typeC
(the constraint) instead of the generic typeT
, which allows us to do narrowing when we couldn't. As an example:Inside the
if
branch above, to get the type ofvalue.a
, we first need the type ofvalue
. So we callgetNarrowableTypeForReference(value)
, and get typeC
(instead of the genericT
, which we can't narrow). Then we can further narrowC
into{ kind: "a", a: string }
due to theif
condition. Finally, we get the type of propertya
of type{ kind: "a", a: string }
, which is typestring
, and that is the type ofvalue.a
.In 4.3, the same thing happened for binding elements in binding patterns:
However, that meant other things broke, like #43941:
In the example above, to determine the type of
...rest
, we once again callgetNarrowableTypeForReference(params)
, determine that type, and then compute whatever properties are left after we removefoo
.Before 4.3,
getNarrowableTypeForReference(params)
returned typeP
, sorest
would have generic typeOmit<P, "foo">. In 4.3,
getNarrowableTypeForReference(params)returned type
Params, so
restwould have non-generic type
{ tag: 'a'; type: number } | { tag: 'b'; type: string }`.That distinction between
rest
having a generic versus non-generic type means we can or can't narrow other uses ofrest
.The problem in the example above surfaces in the
switch
statement, when we try to call the functiongetType
, that returnsrest
.If we keep
rest
generic, we can "flow" the narrowing of the switch statement into the return type ofgetType
.If
rest
is instead the non-generic type, we can't do that narrowing, because there's no longer a connection betweengetType
's type parameterP
and the type ofrest
.Change in 4.4
To fix the issue #43941 above, we reverted the
getNarrowableTypeForReference(ref)
behavior, making it return the type parameter instead of the type parameter's constraint when the referenceref
was contextually typed by binding patterns.So the result was like this:
Issue
It seems we are at odds on whether we should have
getNarrowableTypeForReference
return the generic type or the constraint when we have binding patterns/destructuring, because the most useful behavior can go either way depending on the code in question.In some cases, we really expect the binding pattern access to behave like the regular property access:
On the other hand, in this case:
when
rest
has the generic typeOmit<P, "foo">
, it becomes possibly more useful for callers of that function, because we can narrow the return type based on the argument type.Proposed fix
A compromise for this issue with binding patterns is to have
getNarrowableTypeForReference
return the generic type if we are retrieving the type in order to use it for a rest binding element, and to havegetNarrowableTypeForReference
return the constraint type for non-rest binding elements. Like this:Implementation
To implement this special casing for binding elements, we need a way to know *inside
getNarrowableTypeForReference
if we are returning a type to be used for determining the type of an object rest binding element.This PR changes
getNarrowableTypeForReference
(and some other checker functions) to take in acheckMode
parameter.We also have a new
CheckMode
flag,CheckMode.ObjectRestBindingElement
, that indicates precisely this.Questions
Do we want to move forward with this fix? It's a bit weird and seems like we're stacking a special case on top of a special case, but there might not be a better way to solve this, because an argument can be made for using a generic type's constraint, and an argument can also be made for keeping the generic type in the situations above.
Is there a better way to implement this, without passing around
CheckMode
?Do we want this special, generic-preserving behavior for rest binding elements in array binding patterns too?