diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f15fd70378ea7..6252be60a4993 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -26740,7 +26740,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; } } @@ -26754,6 +26758,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; @@ -28110,57 +28159,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) { diff --git a/tests/baselines/reference/typePredicatesCanNarrowByDiscriminant.js b/tests/baselines/reference/typePredicatesCanNarrowByDiscriminant.js new file mode 100644 index 0000000000000..f1aa36ed6924e --- /dev/null +++ b/tests/baselines/reference/typePredicatesCanNarrowByDiscriminant.js @@ -0,0 +1,30 @@ +//// [tests/cases/compiler/typePredicatesCanNarrowByDiscriminant.ts] //// + +//// [typePredicatesCanNarrowByDiscriminant.ts] +// #45770 +declare const fruit: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' } + +declare function isOneOf(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; +} diff --git a/tests/baselines/reference/typePredicatesCanNarrowByDiscriminant.symbols b/tests/baselines/reference/typePredicatesCanNarrowByDiscriminant.symbols new file mode 100644 index 0000000000000..e1bacc007b26a --- /dev/null +++ b/tests/baselines/reference/typePredicatesCanNarrowByDiscriminant.symbols @@ -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(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)) +} diff --git a/tests/baselines/reference/typePredicatesCanNarrowByDiscriminant.types b/tests/baselines/reference/typePredicatesCanNarrowByDiscriminant.types new file mode 100644 index 0000000000000..3dc7156b1fa08 --- /dev/null +++ b/tests/baselines/reference/typePredicatesCanNarrowByDiscriminant.types @@ -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(item: T, array: readonly U[]): item is U +>isOneOf : (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 : (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 : (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"; } +} diff --git a/tests/cases/compiler/typePredicatesCanNarrowByDiscriminant.ts b/tests/cases/compiler/typePredicatesCanNarrowByDiscriminant.ts new file mode 100644 index 0000000000000..e24a209d6fbe0 --- /dev/null +++ b/tests/cases/compiler/typePredicatesCanNarrowByDiscriminant.ts @@ -0,0 +1,17 @@ +// @strict: true + +// #45770 +declare const fruit: { kind: 'apple'} | { kind: 'banana' } | { kind: 'cherry' } + +declare function isOneOf(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 +} \ No newline at end of file