Bring back return type narrowing + fixes #61359
Open
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Fixes #33014.
Fixes #33912.
This PR brings back conditional and indexed access return type narrowing from #56941, but with a few updates.
The main motivation behind those updates is described in https://gist.github.com/gabritto/b6ebd5f9fc2bb3cfc305027609e66bca,
but to put it shortly, type narrowing very often cannot distinguish between two non-primitive types,
and since return type narrowing depends on type narrowing to work, it often didn't.
Update 1: non-primitive restriction
To deal with those problematic type narrowing scenarios, the first update is to disallow narrowing of conditional return types that
attempt to distinguish between two non-primitive types.
So this example will not work:
That's because the conditional return type
QuickPickReturn
has one branch with type{ canSelectMultiple: true }
, which is non-primitive,and another branch with type
{ canSelectMultiple: false }
, which is also non-primitive.However, the following will work:
Distinguishing between two primitive types or between a primitive and non-primitive type in the conditional type's branches is allowed.
Update 2: type parameter embedding
We can now detect that a type parameter is a candidate for being used in return type narrowing (i.e. it's a narrowable type parameter) in
cases where the type parameter is indirectly used as a type of a parameter or property.
Before, only this type of usage of
T
in a parameter type annotation would be recognized:Now, the following also work:
Combined with the non-primitive restriction mentioned above, this enables users to place narrowable type parameters inside object types
at the exact property that is used for narrowing, so instead of writing this:
function fun<T extends { prop: true } | { prop: false }>(param: T): ...
,users can write this:
function fun<T extends true | false>(param: { prop: T }): ...
.Note that the analysis done to decide if a type parameter is used the parameter in a way that allows it to be narrowed is a syntactical one.
We want to avoid resolving and inspecting actual types during the analysis, because a lot of types are lazy in some sense, and we don't
want this analysis to cause unintended side effects, e.g. circularity errors.
But this means that any usage of a type parameter that requires semantically resolving types to validate it is not going to work.
For a more complete list of what's currently supported here in terms of usages of type parameters, see test
tests\cases\compiler\dependentReturnType11.ts
.Update 3: relax extends type restriction
This is a small improvement unrelated to the previous updates.
In the original PR, this was disallowed because we required that a conditional return type's extends types be identical to the types
in the type parameter constraint:
Note that in
NormalizedRet
, the first branch's extends type is{}
, notDog
, so this wasn't allowed because those types are not identical.With this PR, we only require that a type in the constraint is assignable to the extends type, i.e. that
Dog
is assignable to{}
,so the code above is now allowed for return type narrowing.