mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-19 10:41:56 -05:00
Allow (non-assert) type predicates to narrow by discriminant (#57358)
This commit is contained in:
committed by
GitHub
parent
23960ac88c
commit
a6414052a3
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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"; }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user