Skip to content

Commit a641405

Browse files
authored
Allow (non-assert) type predicates to narrow by discriminant (microsoft#57358)
1 parent 23960ac commit a641405

5 files changed

+225
-47
lines changed

src/compiler/checker.ts

+51-47
Original file line numberDiff line numberDiff line change
@@ -26743,7 +26743,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2674326743
function hasMatchingArgument(expression: CallExpression | NewExpression, reference: Node) {
2674426744
if (expression.arguments) {
2674526745
for (const argument of expression.arguments) {
26746-
if (isOrContainsMatchingReference(reference, argument) || optionalChainContainsReference(argument, reference)) {
26746+
if (
26747+
isOrContainsMatchingReference(reference, argument)
26748+
|| optionalChainContainsReference(argument, reference)
26749+
|| getCandidateDiscriminantPropertyAccess(argument, reference)
26750+
) {
2674726751
return true;
2674826752
}
2674926753
}
@@ -26757,6 +26761,51 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2675726761
return false;
2675826762
}
2675926763

26764+
function getCandidateDiscriminantPropertyAccess(expr: Expression, reference: Node) {
26765+
if (isBindingPattern(reference) || isFunctionExpressionOrArrowFunction(reference) || isObjectLiteralMethod(reference)) {
26766+
// When the reference is a binding pattern or function or arrow expression, we are narrowing a pesudo-reference in
26767+
// getNarrowedTypeOfSymbol. An identifier for a destructuring variable declared in the same binding pattern or
26768+
// parameter declared in the same parameter list is a candidate.
26769+
if (isIdentifier(expr)) {
26770+
const symbol = getResolvedSymbol(expr);
26771+
const declaration = symbol.valueDeclaration;
26772+
if (declaration && (isBindingElement(declaration) || isParameter(declaration)) && reference === declaration.parent && !declaration.initializer && !declaration.dotDotDotToken) {
26773+
return declaration;
26774+
}
26775+
}
26776+
}
26777+
else if (isAccessExpression(expr)) {
26778+
// An access expression is a candidate if the reference matches the left hand expression.
26779+
if (isMatchingReference(reference, expr.expression)) {
26780+
return expr;
26781+
}
26782+
}
26783+
else if (isIdentifier(expr)) {
26784+
const symbol = getResolvedSymbol(expr);
26785+
if (isConstantVariable(symbol)) {
26786+
const declaration = symbol.valueDeclaration!;
26787+
// Given 'const x = obj.kind', allow 'x' as an alias for 'obj.kind'
26788+
if (
26789+
isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && isAccessExpression(declaration.initializer) &&
26790+
isMatchingReference(reference, declaration.initializer.expression)
26791+
) {
26792+
return declaration.initializer;
26793+
}
26794+
// Given 'const { kind: x } = obj', allow 'x' as an alias for 'obj.kind'
26795+
if (isBindingElement(declaration) && !declaration.initializer) {
26796+
const parent = declaration.parent.parent;
26797+
if (
26798+
isVariableDeclaration(parent) && !parent.type && parent.initializer && (isIdentifier(parent.initializer) || isAccessExpression(parent.initializer)) &&
26799+
isMatchingReference(reference, parent.initializer)
26800+
) {
26801+
return declaration;
26802+
}
26803+
}
26804+
}
26805+
}
26806+
return undefined;
26807+
}
26808+
2676026809
function getFlowNodeId(flow: FlowNode): number {
2676126810
if (!flow.id || flow.id < 0) {
2676226811
flow.id = nextFlowId;
@@ -28113,57 +28162,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2811328162
return result;
2811428163
}
2811528164

28116-
function getCandidateDiscriminantPropertyAccess(expr: Expression) {
28117-
if (isBindingPattern(reference) || isFunctionExpressionOrArrowFunction(reference) || isObjectLiteralMethod(reference)) {
28118-
// When the reference is a binding pattern or function or arrow expression, we are narrowing a pesudo-reference in
28119-
// getNarrowedTypeOfSymbol. An identifier for a destructuring variable declared in the same binding pattern or
28120-
// parameter declared in the same parameter list is a candidate.
28121-
if (isIdentifier(expr)) {
28122-
const symbol = getResolvedSymbol(expr);
28123-
const declaration = symbol.valueDeclaration;
28124-
if (declaration && (isBindingElement(declaration) || isParameter(declaration)) && reference === declaration.parent && !declaration.initializer && !declaration.dotDotDotToken) {
28125-
return declaration;
28126-
}
28127-
}
28128-
}
28129-
else if (isAccessExpression(expr)) {
28130-
// An access expression is a candidate if the reference matches the left hand expression.
28131-
if (isMatchingReference(reference, expr.expression)) {
28132-
return expr;
28133-
}
28134-
}
28135-
else if (isIdentifier(expr)) {
28136-
const symbol = getResolvedSymbol(expr);
28137-
if (isConstantVariable(symbol)) {
28138-
const declaration = symbol.valueDeclaration!;
28139-
// Given 'const x = obj.kind', allow 'x' as an alias for 'obj.kind'
28140-
if (
28141-
isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && isAccessExpression(declaration.initializer) &&
28142-
isMatchingReference(reference, declaration.initializer.expression)
28143-
) {
28144-
return declaration.initializer;
28145-
}
28146-
// Given 'const { kind: x } = obj', allow 'x' as an alias for 'obj.kind'
28147-
if (isBindingElement(declaration) && !declaration.initializer) {
28148-
const parent = declaration.parent.parent;
28149-
if (
28150-
isVariableDeclaration(parent) && !parent.type && parent.initializer && (isIdentifier(parent.initializer) || isAccessExpression(parent.initializer)) &&
28151-
isMatchingReference(reference, parent.initializer)
28152-
) {
28153-
return declaration;
28154-
}
28155-
}
28156-
}
28157-
}
28158-
return undefined;
28159-
}
28160-
2816128165
function getDiscriminantPropertyAccess(expr: Expression, computedType: Type) {
2816228166
// As long as the computed type is a subset of the declared type, we use the full declared type to detect
2816328167
// a discriminant property. In cases where the computed type isn't a subset, e.g because of a preceding type
2816428168
// predicate narrowing, we use the actual computed type.
2816528169
if (declaredType.flags & TypeFlags.Union || computedType.flags & TypeFlags.Union) {
28166-
const access = getCandidateDiscriminantPropertyAccess(expr);
28170+
const access = getCandidateDiscriminantPropertyAccess(expr, reference);
2816728171
if (access) {
2816828172
const name = getAccessedPropertyName(access);
2816928173
if (name) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//// [tests/cases/compiler/typePredicatesCanNarrowByDiscriminant.ts] ////
2+
3+
//// [typePredicatesCanNarrowByDiscriminant.ts]
4+
// #45770
5+
declare const fruit: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
6+
7+
declare function isOneOf<T, U extends T>(item: T, array: readonly U[]): item is U
8+
if (isOneOf(fruit.kind, ['apple', 'banana'] as const)) {
9+
fruit.kind
10+
fruit
11+
}
12+
13+
declare const fruit2: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
14+
const kind = fruit2.kind;
15+
if (isOneOf(kind, ['apple', 'banana'] as const)) {
16+
fruit2.kind
17+
fruit2
18+
}
19+
20+
//// [typePredicatesCanNarrowByDiscriminant.js]
21+
"use strict";
22+
if (isOneOf(fruit.kind, ['apple', 'banana'])) {
23+
fruit.kind;
24+
fruit;
25+
}
26+
var kind = fruit2.kind;
27+
if (isOneOf(kind, ['apple', 'banana'])) {
28+
fruit2.kind;
29+
fruit2;
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//// [tests/cases/compiler/typePredicatesCanNarrowByDiscriminant.ts] ////
2+
3+
=== typePredicatesCanNarrowByDiscriminant.ts ===
4+
// #45770
5+
declare const fruit: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
6+
>fruit : Symbol(fruit, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 13))
7+
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 22))
8+
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 41))
9+
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 62))
10+
11+
declare function isOneOf<T, U extends T>(item: T, array: readonly U[]): item is U
12+
>isOneOf : Symbol(isOneOf, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 79))
13+
>T : Symbol(T, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 25))
14+
>U : Symbol(U, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 27))
15+
>T : Symbol(T, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 25))
16+
>item : Symbol(item, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 41))
17+
>T : Symbol(T, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 25))
18+
>array : Symbol(array, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 49))
19+
>U : Symbol(U, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 27))
20+
>item : Symbol(item, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 41))
21+
>U : Symbol(U, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 27))
22+
23+
if (isOneOf(fruit.kind, ['apple', 'banana'] as const)) {
24+
>isOneOf : Symbol(isOneOf, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 79))
25+
>fruit.kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 22), Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 41), Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 62))
26+
>fruit : Symbol(fruit, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 13))
27+
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 22), Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 41), Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 62))
28+
>const : Symbol(const)
29+
30+
fruit.kind
31+
>fruit.kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 22), Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 41))
32+
>fruit : Symbol(fruit, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 13))
33+
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 22), Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 41))
34+
35+
fruit
36+
>fruit : Symbol(fruit, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 13))
37+
}
38+
39+
declare const fruit2: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
40+
>fruit2 : Symbol(fruit2, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 13))
41+
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 23))
42+
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 42))
43+
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 63))
44+
45+
const kind = fruit2.kind;
46+
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 10, 5))
47+
>fruit2.kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 23), Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 42), Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 63))
48+
>fruit2 : Symbol(fruit2, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 13))
49+
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 23), Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 42), Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 63))
50+
51+
if (isOneOf(kind, ['apple', 'banana'] as const)) {
52+
>isOneOf : Symbol(isOneOf, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 79))
53+
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 10, 5))
54+
>const : Symbol(const)
55+
56+
fruit2.kind
57+
>fruit2.kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 23), Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 42))
58+
>fruit2 : Symbol(fruit2, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 13))
59+
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 23), Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 42))
60+
61+
fruit2
62+
>fruit2 : Symbol(fruit2, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 13))
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//// [tests/cases/compiler/typePredicatesCanNarrowByDiscriminant.ts] ////
2+
3+
=== typePredicatesCanNarrowByDiscriminant.ts ===
4+
// #45770
5+
declare const fruit: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
6+
>fruit : { kind: 'apple'; } | { kind: 'banana'; } | { kind: 'cherry'; }
7+
>kind : "apple"
8+
>kind : "banana"
9+
>kind : "cherry"
10+
11+
declare function isOneOf<T, U extends T>(item: T, array: readonly U[]): item is U
12+
>isOneOf : <T, U extends T>(item: T, array: readonly U[]) => item is U
13+
>item : T
14+
>array : readonly U[]
15+
16+
if (isOneOf(fruit.kind, ['apple', 'banana'] as const)) {
17+
>isOneOf(fruit.kind, ['apple', 'banana'] as const) : boolean
18+
>isOneOf : <T, U extends T>(item: T, array: readonly U[]) => item is U
19+
>fruit.kind : "apple" | "banana" | "cherry"
20+
>fruit : { kind: "apple"; } | { kind: "banana"; } | { kind: "cherry"; }
21+
>kind : "apple" | "banana" | "cherry"
22+
>['apple', 'banana'] as const : readonly ["apple", "banana"]
23+
>['apple', 'banana'] : readonly ["apple", "banana"]
24+
>'apple' : "apple"
25+
>'banana' : "banana"
26+
27+
fruit.kind
28+
>fruit.kind : "apple" | "banana"
29+
>fruit : { kind: "apple"; } | { kind: "banana"; }
30+
>kind : "apple" | "banana"
31+
32+
fruit
33+
>fruit : { kind: "apple"; } | { kind: "banana"; }
34+
}
35+
36+
declare const fruit2: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
37+
>fruit2 : { kind: 'apple'; } | { kind: 'banana'; } | { kind: 'cherry'; }
38+
>kind : "apple"
39+
>kind : "banana"
40+
>kind : "cherry"
41+
42+
const kind = fruit2.kind;
43+
>kind : "apple" | "banana" | "cherry"
44+
>fruit2.kind : "apple" | "banana" | "cherry"
45+
>fruit2 : { kind: "apple"; } | { kind: "banana"; } | { kind: "cherry"; }
46+
>kind : "apple" | "banana" | "cherry"
47+
48+
if (isOneOf(kind, ['apple', 'banana'] as const)) {
49+
>isOneOf(kind, ['apple', 'banana'] as const) : boolean
50+
>isOneOf : <T, U extends T>(item: T, array: readonly U[]) => item is U
51+
>kind : "apple" | "banana" | "cherry"
52+
>['apple', 'banana'] as const : readonly ["apple", "banana"]
53+
>['apple', 'banana'] : readonly ["apple", "banana"]
54+
>'apple' : "apple"
55+
>'banana' : "banana"
56+
57+
fruit2.kind
58+
>fruit2.kind : "apple" | "banana"
59+
>fruit2 : { kind: "apple"; } | { kind: "banana"; }
60+
>kind : "apple" | "banana"
61+
62+
fruit2
63+
>fruit2 : { kind: "apple"; } | { kind: "banana"; }
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// @strict: true
2+
3+
// #45770
4+
declare const fruit: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
5+
6+
declare function isOneOf<T, U extends T>(item: T, array: readonly U[]): item is U
7+
if (isOneOf(fruit.kind, ['apple', 'banana'] as const)) {
8+
fruit.kind
9+
fruit
10+
}
11+
12+
declare const fruit2: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
13+
const kind = fruit2.kind;
14+
if (isOneOf(kind, ['apple', 'banana'] as const)) {
15+
fruit2.kind
16+
fruit2
17+
}

0 commit comments

Comments
 (0)