diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ee074403b1b9f..5a18b79776944 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -20265,12 +20265,6 @@ namespace ts { if (right.kind === SyntaxKind.TypeOfExpression && isStringLiteralLike(left)) { return narrowTypeByTypeof(type, right, operator, left, assumeTrue); } - if (isConstructorAccessExpression(left)) { - return narrowTypeByConstructor(type, left, operator, right, assumeTrue); - } - if (isConstructorAccessExpression(right)) { - return narrowTypeByConstructor(type, right, operator, left, assumeTrue); - } if (isMatchingReference(reference, left)) { return narrowTypeByEquality(type, operator, right, assumeTrue); } @@ -20291,6 +20285,12 @@ namespace ts { if (isMatchingReferenceDiscriminant(right, declaredType)) { return narrowTypeByDiscriminant(type, right, t => narrowTypeByEquality(t, operator, left, assumeTrue)); } + if (isMatchingConstructorReference(left)) { + return narrowTypeByConstructor(type, operator, right, assumeTrue); + } + if (isMatchingConstructorReference(right)) { + return narrowTypeByConstructor(type, operator, left, assumeTrue); + } break; case SyntaxKind.InstanceOfKeyword: return narrowTypeByInstanceof(type, expr, assumeTrue); @@ -20564,17 +20564,18 @@ namespace ts { return getTypeWithFacts(mapType(type, narrowTypeForTypeofSwitch(impliedType)), switchFacts); } - function narrowTypeByConstructor(type: Type, constructorAccessExpr: AccessExpression, operator: SyntaxKind, identifier: Expression, assumeTrue: boolean): Type { + function isMatchingConstructorReference(expr: Expression) { + return (isPropertyAccessExpression(expr) && idText(expr.name) === "constructor" || + isElementAccessExpression(expr) && isStringLiteralLike(expr.argumentExpression) && expr.argumentExpression.text === "constructor") && + isMatchingReference(reference, expr.expression); + } + + function narrowTypeByConstructor(type: Type, operator: SyntaxKind, identifier: Expression, assumeTrue: boolean): Type { // Do not narrow when checking inequality. if (assumeTrue ? (operator !== SyntaxKind.EqualsEqualsToken && operator !== SyntaxKind.EqualsEqualsEqualsToken) : (operator !== SyntaxKind.ExclamationEqualsToken && operator !== SyntaxKind.ExclamationEqualsEqualsToken)) { return type; } - // In the case of `x.y`, a `x.constructor === T` type guard resets the narrowed type of `y` to its declared type. - if (!isMatchingReference(reference, constructorAccessExpr.expression)) { - return declaredType; - } - // Get the type of the constructor identifier expression, if it is not a function then do not narrow. const identifierType = getTypeOfExpression(identifier); if (!isFunctionType(identifierType) && !isConstructorType(identifierType)) { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 1065eaf0412d0..2c6f8b70a34b1 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4432,13 +4432,6 @@ namespace ts { return isPropertyAccessExpression(node) && isEntityNameExpression(node.expression); } - export function isConstructorAccessExpression(expr: Expression): expr is AccessExpression { - return ( - isPropertyAccessExpression(expr) && idText(expr.name) === "constructor" || - isElementAccessExpression(expr) && isStringLiteralLike(expr.argumentExpression) && expr.argumentExpression.text === "constructor" - ); - } - export function tryGetPropertyAccessOrIdentifierToString(expr: Expression): string | undefined { if (isPropertyAccessExpression(expr)) { const baseStr = tryGetPropertyAccessOrIdentifierToString(expr.expression); diff --git a/tests/baselines/reference/typeGuardConstructorClassAndNumber.errors.txt b/tests/baselines/reference/typeGuardConstructorClassAndNumber.errors.txt index b356fddd14ef4..c9f017843c163 100644 --- a/tests/baselines/reference/typeGuardConstructorClassAndNumber.errors.txt +++ b/tests/baselines/reference/typeGuardConstructorClassAndNumber.errors.txt @@ -160,4 +160,14 @@ tests/cases/compiler/typeGuardConstructorClassAndNumber.ts(115,10): error TS2339 else { var1; // C1 } + + // Repro from #37660 + + function foo(instance: Function | object) { + if (typeof instance === 'function') { + if (instance.prototype == null || instance.prototype.constructor == null) { + return instance.length; + } + } + } \ No newline at end of file diff --git a/tests/baselines/reference/typeGuardConstructorClassAndNumber.js b/tests/baselines/reference/typeGuardConstructorClassAndNumber.js index 188de4cf06733..8034d6b4517d1 100644 --- a/tests/baselines/reference/typeGuardConstructorClassAndNumber.js +++ b/tests/baselines/reference/typeGuardConstructorClassAndNumber.js @@ -118,6 +118,16 @@ if (C1 !== var1["constructor"]) { else { var1; // C1 } + +// Repro from #37660 + +function foo(instance: Function | object) { + if (typeof instance === 'function') { + if (instance.prototype == null || instance.prototype.constructor == null) { + return instance.length; + } + } +} //// [typeGuardConstructorClassAndNumber.js] @@ -240,3 +250,11 @@ if (C1 !== var1["constructor"]) { else { var1; // C1 } +// Repro from #37660 +function foo(instance) { + if (typeof instance === 'function') { + if (instance.prototype == null || instance.prototype.constructor == null) { + return instance.length; + } + } +} diff --git a/tests/baselines/reference/typeGuardConstructorClassAndNumber.symbols b/tests/baselines/reference/typeGuardConstructorClassAndNumber.symbols index 893ad03fad252..033b0fb2d550a 100644 --- a/tests/baselines/reference/typeGuardConstructorClassAndNumber.symbols +++ b/tests/baselines/reference/typeGuardConstructorClassAndNumber.symbols @@ -277,3 +277,29 @@ else { >var1 : Symbol(var1, Decl(typeGuardConstructorClassAndNumber.ts, 5, 3)) } +// Repro from #37660 + +function foo(instance: Function | object) { +>foo : Symbol(foo, Decl(typeGuardConstructorClassAndNumber.ts, 118, 1)) +>instance : Symbol(instance, Decl(typeGuardConstructorClassAndNumber.ts, 122, 13)) +>Function : Symbol(Function, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + + if (typeof instance === 'function') { +>instance : Symbol(instance, Decl(typeGuardConstructorClassAndNumber.ts, 122, 13)) + + if (instance.prototype == null || instance.prototype.constructor == null) { +>instance.prototype : Symbol(Function.prototype, Decl(lib.es5.d.ts, --, --)) +>instance : Symbol(instance, Decl(typeGuardConstructorClassAndNumber.ts, 122, 13)) +>prototype : Symbol(Function.prototype, Decl(lib.es5.d.ts, --, --)) +>instance.prototype : Symbol(Function.prototype, Decl(lib.es5.d.ts, --, --)) +>instance : Symbol(instance, Decl(typeGuardConstructorClassAndNumber.ts, 122, 13)) +>prototype : Symbol(Function.prototype, Decl(lib.es5.d.ts, --, --)) + + return instance.length; +>instance.length : Symbol(Function.length, Decl(lib.es5.d.ts, --, --)) +>instance : Symbol(instance, Decl(typeGuardConstructorClassAndNumber.ts, 122, 13)) +>length : Symbol(Function.length, Decl(lib.es5.d.ts, --, --)) + } + } +} + diff --git a/tests/baselines/reference/typeGuardConstructorClassAndNumber.types b/tests/baselines/reference/typeGuardConstructorClassAndNumber.types index 1c4c2fb523091..d3ecf5d11e9cb 100644 --- a/tests/baselines/reference/typeGuardConstructorClassAndNumber.types +++ b/tests/baselines/reference/typeGuardConstructorClassAndNumber.types @@ -316,3 +316,38 @@ else { >var1 : C1 } +// Repro from #37660 + +function foo(instance: Function | object) { +>foo : (instance: object | Function) => number +>instance : object | Function + + if (typeof instance === 'function') { +>typeof instance === 'function' : boolean +>typeof instance : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" +>instance : object | Function +>'function' : "function" + + if (instance.prototype == null || instance.prototype.constructor == null) { +>instance.prototype == null || instance.prototype.constructor == null : boolean +>instance.prototype == null : boolean +>instance.prototype : any +>instance : Function +>prototype : any +>null : null +>instance.prototype.constructor == null : boolean +>instance.prototype.constructor : any +>instance.prototype : any +>instance : Function +>prototype : any +>constructor : any +>null : null + + return instance.length; +>instance.length : number +>instance : Function +>length : number + } + } +} + diff --git a/tests/cases/compiler/typeGuardConstructorClassAndNumber.ts b/tests/cases/compiler/typeGuardConstructorClassAndNumber.ts index 10ce6afdff05f..9a6038a9d0197 100644 --- a/tests/cases/compiler/typeGuardConstructorClassAndNumber.ts +++ b/tests/cases/compiler/typeGuardConstructorClassAndNumber.ts @@ -117,3 +117,13 @@ if (C1 !== var1["constructor"]) { else { var1; // C1 } + +// Repro from #37660 + +function foo(instance: Function | object) { + if (typeof instance === 'function') { + if (instance.prototype == null || instance.prototype.constructor == null) { + return instance.length; + } + } +}