Allow (non-assert) type predicates to narrow by discriminant (#57358)

This commit is contained in:
Gabriela Araujo Britto
2024-02-13 11:33:30 -08:00
committed by GitHub
parent 23960ac88c
commit a6414052a3
5 changed files with 225 additions and 47 deletions

View File

@@ -26743,7 +26743,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function hasMatchingArgument(expression: CallExpression | NewExpression, reference: Node) {
if (expression.arguments) {
for (const argument of expression.arguments) {
if (isOrContainsMatchingReference(reference, argument) || optionalChainContainsReference(argument, reference)) {
if (
isOrContainsMatchingReference(reference, argument)
|| optionalChainContainsReference(argument, reference)
|| getCandidateDiscriminantPropertyAccess(argument, reference)
) {
return true;
}
}
@@ -26757,6 +26761,51 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return false;
}
function getCandidateDiscriminantPropertyAccess(expr: Expression, reference: Node) {
if (isBindingPattern(reference) || isFunctionExpressionOrArrowFunction(reference) || isObjectLiteralMethod(reference)) {
// When the reference is a binding pattern or function or arrow expression, we are narrowing a pesudo-reference in
// getNarrowedTypeOfSymbol. An identifier for a destructuring variable declared in the same binding pattern or
// parameter declared in the same parameter list is a candidate.
if (isIdentifier(expr)) {
const symbol = getResolvedSymbol(expr);
const declaration = symbol.valueDeclaration;
if (declaration && (isBindingElement(declaration) || isParameter(declaration)) && reference === declaration.parent && !declaration.initializer && !declaration.dotDotDotToken) {
return declaration;
}
}
}
else if (isAccessExpression(expr)) {
// An access expression is a candidate if the reference matches the left hand expression.
if (isMatchingReference(reference, expr.expression)) {
return expr;
}
}
else if (isIdentifier(expr)) {
const symbol = getResolvedSymbol(expr);
if (isConstantVariable(symbol)) {
const declaration = symbol.valueDeclaration!;
// Given 'const x = obj.kind', allow 'x' as an alias for 'obj.kind'
if (
isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && isAccessExpression(declaration.initializer) &&
isMatchingReference(reference, declaration.initializer.expression)
) {
return declaration.initializer;
}
// Given 'const { kind: x } = obj', allow 'x' as an alias for 'obj.kind'
if (isBindingElement(declaration) && !declaration.initializer) {
const parent = declaration.parent.parent;
if (
isVariableDeclaration(parent) && !parent.type && parent.initializer && (isIdentifier(parent.initializer) || isAccessExpression(parent.initializer)) &&
isMatchingReference(reference, parent.initializer)
) {
return declaration;
}
}
}
}
return undefined;
}
function getFlowNodeId(flow: FlowNode): number {
if (!flow.id || flow.id < 0) {
flow.id = nextFlowId;
@@ -28113,57 +28162,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return result;
}
function getCandidateDiscriminantPropertyAccess(expr: Expression) {
if (isBindingPattern(reference) || isFunctionExpressionOrArrowFunction(reference) || isObjectLiteralMethod(reference)) {
// When the reference is a binding pattern or function or arrow expression, we are narrowing a pesudo-reference in
// getNarrowedTypeOfSymbol. An identifier for a destructuring variable declared in the same binding pattern or
// parameter declared in the same parameter list is a candidate.
if (isIdentifier(expr)) {
const symbol = getResolvedSymbol(expr);
const declaration = symbol.valueDeclaration;
if (declaration && (isBindingElement(declaration) || isParameter(declaration)) && reference === declaration.parent && !declaration.initializer && !declaration.dotDotDotToken) {
return declaration;
}
}
}
else if (isAccessExpression(expr)) {
// An access expression is a candidate if the reference matches the left hand expression.
if (isMatchingReference(reference, expr.expression)) {
return expr;
}
}
else if (isIdentifier(expr)) {
const symbol = getResolvedSymbol(expr);
if (isConstantVariable(symbol)) {
const declaration = symbol.valueDeclaration!;
// Given 'const x = obj.kind', allow 'x' as an alias for 'obj.kind'
if (
isVariableDeclaration(declaration) && !declaration.type && declaration.initializer && isAccessExpression(declaration.initializer) &&
isMatchingReference(reference, declaration.initializer.expression)
) {
return declaration.initializer;
}
// Given 'const { kind: x } = obj', allow 'x' as an alias for 'obj.kind'
if (isBindingElement(declaration) && !declaration.initializer) {
const parent = declaration.parent.parent;
if (
isVariableDeclaration(parent) && !parent.type && parent.initializer && (isIdentifier(parent.initializer) || isAccessExpression(parent.initializer)) &&
isMatchingReference(reference, parent.initializer)
) {
return declaration;
}
}
}
}
return undefined;
}
function getDiscriminantPropertyAccess(expr: Expression, computedType: Type) {
// As long as the computed type is a subset of the declared type, we use the full declared type to detect
// a discriminant property. In cases where the computed type isn't a subset, e.g because of a preceding type
// predicate narrowing, we use the actual computed type.
if (declaredType.flags & TypeFlags.Union || computedType.flags & TypeFlags.Union) {
const access = getCandidateDiscriminantPropertyAccess(expr);
const access = getCandidateDiscriminantPropertyAccess(expr, reference);
if (access) {
const name = getAccessedPropertyName(access);
if (name) {

View File

@@ -0,0 +1,30 @@
//// [tests/cases/compiler/typePredicatesCanNarrowByDiscriminant.ts] ////
//// [typePredicatesCanNarrowByDiscriminant.ts]
// #45770
declare const fruit: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
declare function isOneOf<T, U extends T>(item: T, array: readonly U[]): item is U
if (isOneOf(fruit.kind, ['apple', 'banana'] as const)) {
fruit.kind
fruit
}
declare const fruit2: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
const kind = fruit2.kind;
if (isOneOf(kind, ['apple', 'banana'] as const)) {
fruit2.kind
fruit2
}
//// [typePredicatesCanNarrowByDiscriminant.js]
"use strict";
if (isOneOf(fruit.kind, ['apple', 'banana'])) {
fruit.kind;
fruit;
}
var kind = fruit2.kind;
if (isOneOf(kind, ['apple', 'banana'])) {
fruit2.kind;
fruit2;
}

View File

@@ -0,0 +1,63 @@
//// [tests/cases/compiler/typePredicatesCanNarrowByDiscriminant.ts] ////
=== typePredicatesCanNarrowByDiscriminant.ts ===
// #45770
declare const fruit: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
>fruit : Symbol(fruit, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 13))
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 22))
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 41))
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 62))
declare function isOneOf<T, U extends T>(item: T, array: readonly U[]): item is U
>isOneOf : Symbol(isOneOf, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 79))
>T : Symbol(T, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 25))
>U : Symbol(U, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 27))
>T : Symbol(T, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 25))
>item : Symbol(item, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 41))
>T : Symbol(T, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 25))
>array : Symbol(array, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 49))
>U : Symbol(U, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 27))
>item : Symbol(item, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 41))
>U : Symbol(U, Decl(typePredicatesCanNarrowByDiscriminant.ts, 3, 27))
if (isOneOf(fruit.kind, ['apple', 'banana'] as const)) {
>isOneOf : Symbol(isOneOf, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 79))
>fruit.kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 22), Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 41), Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 62))
>fruit : Symbol(fruit, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 13))
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 22), Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 41), Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 62))
>const : Symbol(const)
fruit.kind
>fruit.kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 22), Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 41))
>fruit : Symbol(fruit, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 13))
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 22), Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 41))
fruit
>fruit : Symbol(fruit, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 13))
}
declare const fruit2: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
>fruit2 : Symbol(fruit2, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 13))
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 23))
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 42))
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 63))
const kind = fruit2.kind;
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 10, 5))
>fruit2.kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 23), Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 42), Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 63))
>fruit2 : Symbol(fruit2, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 13))
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 23), Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 42), Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 63))
if (isOneOf(kind, ['apple', 'banana'] as const)) {
>isOneOf : Symbol(isOneOf, Decl(typePredicatesCanNarrowByDiscriminant.ts, 1, 79))
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 10, 5))
>const : Symbol(const)
fruit2.kind
>fruit2.kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 23), Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 42))
>fruit2 : Symbol(fruit2, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 13))
>kind : Symbol(kind, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 23), Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 42))
fruit2
>fruit2 : Symbol(fruit2, Decl(typePredicatesCanNarrowByDiscriminant.ts, 9, 13))
}

View File

@@ -0,0 +1,64 @@
//// [tests/cases/compiler/typePredicatesCanNarrowByDiscriminant.ts] ////
=== typePredicatesCanNarrowByDiscriminant.ts ===
// #45770
declare const fruit: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
>fruit : { kind: 'apple'; } | { kind: 'banana'; } | { kind: 'cherry'; }
>kind : "apple"
>kind : "banana"
>kind : "cherry"
declare function isOneOf<T, U extends T>(item: T, array: readonly U[]): item is U
>isOneOf : <T, U extends T>(item: T, array: readonly U[]) => item is U
>item : T
>array : readonly U[]
if (isOneOf(fruit.kind, ['apple', 'banana'] as const)) {
>isOneOf(fruit.kind, ['apple', 'banana'] as const) : boolean
>isOneOf : <T, U extends T>(item: T, array: readonly U[]) => item is U
>fruit.kind : "apple" | "banana" | "cherry"
>fruit : { kind: "apple"; } | { kind: "banana"; } | { kind: "cherry"; }
>kind : "apple" | "banana" | "cherry"
>['apple', 'banana'] as const : readonly ["apple", "banana"]
>['apple', 'banana'] : readonly ["apple", "banana"]
>'apple' : "apple"
>'banana' : "banana"
fruit.kind
>fruit.kind : "apple" | "banana"
>fruit : { kind: "apple"; } | { kind: "banana"; }
>kind : "apple" | "banana"
fruit
>fruit : { kind: "apple"; } | { kind: "banana"; }
}
declare const fruit2: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
>fruit2 : { kind: 'apple'; } | { kind: 'banana'; } | { kind: 'cherry'; }
>kind : "apple"
>kind : "banana"
>kind : "cherry"
const kind = fruit2.kind;
>kind : "apple" | "banana" | "cherry"
>fruit2.kind : "apple" | "banana" | "cherry"
>fruit2 : { kind: "apple"; } | { kind: "banana"; } | { kind: "cherry"; }
>kind : "apple" | "banana" | "cherry"
if (isOneOf(kind, ['apple', 'banana'] as const)) {
>isOneOf(kind, ['apple', 'banana'] as const) : boolean
>isOneOf : <T, U extends T>(item: T, array: readonly U[]) => item is U
>kind : "apple" | "banana" | "cherry"
>['apple', 'banana'] as const : readonly ["apple", "banana"]
>['apple', 'banana'] : readonly ["apple", "banana"]
>'apple' : "apple"
>'banana' : "banana"
fruit2.kind
>fruit2.kind : "apple" | "banana"
>fruit2 : { kind: "apple"; } | { kind: "banana"; }
>kind : "apple" | "banana"
fruit2
>fruit2 : { kind: "apple"; } | { kind: "banana"; }
}

View File

@@ -0,0 +1,17 @@
// @strict: true
// #45770
declare const fruit: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
declare function isOneOf<T, U extends T>(item: T, array: readonly U[]): item is U
if (isOneOf(fruit.kind, ['apple', 'banana'] as const)) {
fruit.kind
fruit
}
declare const fruit2: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' }
const kind = fruit2.kind;
if (isOneOf(kind, ['apple', 'banana'] as const)) {
fruit2.kind
fruit2
}