diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 312d8d1db0f02..bccd6075b971a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -668,7 +668,6 @@ import { isOutermostOptionalChain, isParameter, isParameterDeclaration, - isParameterOrCatchClauseVariable, isParameterPropertyDeclaration, isParenthesizedExpression, isParenthesizedTypeNode, @@ -27481,7 +27480,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { case SyntaxKind.Identifier: if (!isThisInTypeQuery(node)) { const symbol = getResolvedSymbol(node as Identifier); - return isConstantVariable(symbol) || isParameterOrCatchClauseVariable(symbol) && !isSymbolAssigned(symbol); + return isConstantVariable(symbol) || isParameterOrMutableLocalVariable(symbol) && !isSymbolAssigned(symbol); } break; case SyntaxKind.PropertyAccessExpression: @@ -28760,10 +28759,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // Check if a parameter or catch variable is assigned anywhere function isSymbolAssigned(symbol: Symbol) { - if (!symbol.valueDeclaration) { + return !isPastLastAssignment(symbol, /*location*/ undefined); + } + + // Return true if there are no assignments to the given symbol or if the given location + // is past the last assignment to the symbol. + function isPastLastAssignment(symbol: Symbol, location: Node | undefined) { + const parent = findAncestor(symbol.valueDeclaration, isFunctionOrSourceFile); + if (!parent) { return false; } - const parent = getRootDeclaration(symbol.valueDeclaration).parent; const links = getNodeLinks(parent); if (!(links.flags & NodeCheckFlags.AssignmentsMarked)) { links.flags |= NodeCheckFlags.AssignmentsMarked; @@ -28771,7 +28776,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { markNodeAssignments(parent); } } - return symbol.isAssigned || false; + return !symbol.lastAssignmentPos || location && symbol.lastAssignmentPos < location.pos; } // Check if a parameter or catch variable (or their bindings elements) is assigned anywhere @@ -28789,27 +28794,98 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } function hasParentWithAssignmentsMarked(node: Node) { - return !!findAncestor(node.parent, node => (isFunctionLike(node) || isCatchClause(node)) && !!(getNodeLinks(node).flags & NodeCheckFlags.AssignmentsMarked)); + return !!findAncestor(node.parent, node => isFunctionOrSourceFile(node) && !!(getNodeLinks(node).flags & NodeCheckFlags.AssignmentsMarked)); } + function isFunctionOrSourceFile(node: Node) { + return isFunctionLikeDeclaration(node) || isSourceFile(node); + } + + // For all assignments within the given root node, record the last assignment source position for all + // referenced parameters and mutable local variables. When assignments occur in nested functions or + // references occur in export specifiers, record Number.MAX_VALUE as the assignment position. When + // assignments occur in compound statements, record the ending source position of the compound statement + // as the assignment position (this is more conservative than full control flow analysis, but requires + // only a single walk over the AST). function markNodeAssignments(node: Node) { - if (node.kind === SyntaxKind.Identifier) { - if (isAssignmentTarget(node)) { - const symbol = getResolvedSymbol(node as Identifier); - if (isParameterOrCatchClauseVariable(symbol)) { - symbol.isAssigned = true; + switch (node.kind) { + case SyntaxKind.Identifier: + if (isAssignmentTarget(node)) { + const symbol = getResolvedSymbol(node as Identifier); + if (isParameterOrMutableLocalVariable(symbol) && symbol.lastAssignmentPos !== Number.MAX_VALUE) { + const referencingFunction = findAncestor(node, isFunctionOrSourceFile); + const declaringFunction = findAncestor(symbol.valueDeclaration, isFunctionOrSourceFile); + symbol.lastAssignmentPos = referencingFunction === declaringFunction ? extendAssignmentPosition(node, symbol.valueDeclaration!) : Number.MAX_VALUE; + } } - } + return; + case SyntaxKind.ExportSpecifier: + const exportDeclaration = (node as ExportSpecifier).parent.parent; + if (!(node as ExportSpecifier).isTypeOnly && !exportDeclaration.isTypeOnly && !exportDeclaration.moduleSpecifier) { + const symbol = resolveEntityName((node as ExportSpecifier).propertyName || (node as ExportSpecifier).name, SymbolFlags.Value, /*ignoreErrors*/ true, /*dontResolveAlias*/ true); + if (symbol && isParameterOrMutableLocalVariable(symbol)) { + symbol.lastAssignmentPos = Number.MAX_VALUE; + } + } + return; + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.EnumDeclaration: + return; } - else { - forEachChild(node, markNodeAssignments); + if (isTypeNode(node)) { + return; } + forEachChild(node, markNodeAssignments); + } + + // Extend the position of the given assignment target node to the end of any intervening variable statement, + // expression statement, compound statement, or class declaration occurring between the node and the given + // declaration node. + function extendAssignmentPosition(node: Node, declaration: Declaration) { + let pos = node.pos; + while (node && node.pos > declaration.pos) { + switch (node.kind) { + case SyntaxKind.VariableStatement: + case SyntaxKind.ExpressionStatement: + case SyntaxKind.IfStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.WithStatement: + case SyntaxKind.SwitchStatement: + case SyntaxKind.TryStatement: + case SyntaxKind.ClassDeclaration: + pos = node.end; + } + node = node.parent; + } + return pos; } function isConstantVariable(symbol: Symbol) { return symbol.flags & SymbolFlags.Variable && (getDeclarationNodeFlagsFromSymbol(symbol) & NodeFlags.Constant) !== 0; } + function isParameterOrMutableLocalVariable(symbol: Symbol) { + // Return true if symbol is a parameter, a catch clause variable, or a mutable local variable + const declaration = symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration); + return !!declaration && ( + isParameter(declaration) || + isVariableDeclaration(declaration) && (isCatchClause(declaration.parent) || isMutableLocalVariableDeclaration(declaration)) + ); + } + + function isMutableLocalVariableDeclaration(declaration: VariableDeclaration) { + // Return true if symbol is a non-exported and non-global `let` variable + return !!(declaration.parent.flags & NodeFlags.Let) && !( + getCombinedModifierFlags(declaration) & ModifierFlags.Export || + declaration.parent.parent.kind === SyntaxKind.VariableStatement && isGlobalSourceFile(declaration.parent.parent.parent) + ); + } + function parameterInitializerContainsUndefined(declaration: ParameterDeclaration): boolean { const links = getNodeLinks(declaration); @@ -29160,13 +29236,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const isModuleExports = symbol.flags & SymbolFlags.ModuleExports; const typeIsAutomatic = type === autoType || type === autoArrayType; const isAutomaticTypeInNonNull = typeIsAutomatic && node.parent.kind === SyntaxKind.NonNullExpression; - // When the control flow originates in a function expression or arrow function and we are referencing - // a const variable or parameter from an outer function, we extend the origin of the control flow - // analysis to include the immediately enclosing function. + // When the control flow originates in a function expression, arrow function, method, or accessor, and + // we are referencing a closed-over const variable or parameter or mutable local variable past its last + // assignment, we extend the origin of the control flow analysis to include the immediately enclosing + // control flow container. while ( - flowContainer !== declarationContainer && (flowContainer.kind === SyntaxKind.FunctionExpression || - flowContainer.kind === SyntaxKind.ArrowFunction || isObjectLiteralOrClassExpressionMethodOrAccessor(flowContainer)) && - (isConstantVariable(localOrExportSymbol) && type !== autoArrayType || isParameter && !isSymbolAssigned(localOrExportSymbol)) + flowContainer !== declarationContainer && ( + flowContainer.kind === SyntaxKind.FunctionExpression || + flowContainer.kind === SyntaxKind.ArrowFunction || + isObjectLiteralOrClassExpressionMethodOrAccessor(flowContainer) + ) && ( + isConstantVariable(localOrExportSymbol) && type !== autoArrayType || + isParameterOrMutableLocalVariable(localOrExportSymbol) && isPastLastAssignment(localOrExportSymbol, node) + ) ) { flowContainer = getControlFlowContainer(flowContainer); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index e56bba5ab4859..0376471ced20b 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5825,8 +5825,8 @@ export interface Symbol { /** @internal */ exportSymbol?: Symbol; // Exported symbol associated with this symbol /** @internal */ constEnumOnlyModule: boolean | undefined; // True if module contains only const enums or other modules with only const enums /** @internal */ isReferenced?: SymbolFlags; // True if the symbol is referenced elsewhere. Keeps track of the meaning of a reference in case a symbol is both a type parameter and parameter. + /** @internal */ lastAssignmentPos?: number; // Source position of last node that assigns value to symbol /** @internal */ isReplaceableByMethod?: boolean; // Can this Javascript class property be replaced by a method symbol? - /** @internal */ isAssigned?: boolean; // True if the symbol is a parameter with assignments /** @internal */ assignmentDeclarationMembers?: Map; // detected late-bound assignment declarations associated with the symbol } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 398b7f1e61305..843249e4bba29 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -8178,7 +8178,7 @@ function Symbol(this: Symbol, flags: SymbolFlags, name: __String) { this.exportSymbol = undefined; this.constEnumOnlyModule = undefined; this.isReferenced = undefined; - this.isAssigned = undefined; + this.lastAssignmentPos = undefined; (this as any).links = undefined; // used by TransientSymbol } @@ -10351,12 +10351,6 @@ export function isCatchClauseVariableDeclaration(node: Node) { return node.kind === SyntaxKind.VariableDeclaration && node.parent.kind === SyntaxKind.CatchClause; } -/** @internal */ -export function isParameterOrCatchClauseVariable(symbol: Symbol) { - const declaration = symbol.valueDeclaration && getRootDeclaration(symbol.valueDeclaration); - return !!declaration && (isParameter(declaration) || isCatchClauseVariableDeclaration(declaration)); -} - /** @internal */ export function isFunctionExpressionOrArrowFunction(node: Node): node is FunctionExpression | ArrowFunction { return node.kind === SyntaxKind.FunctionExpression || node.kind === SyntaxKind.ArrowFunction; diff --git a/tests/baselines/reference/controlFlowAliasing.errors.txt b/tests/baselines/reference/controlFlowAliasing.errors.txt index c062509c6b643..b1dcf7be43f9c 100644 --- a/tests/baselines/reference/controlFlowAliasing.errors.txt +++ b/tests/baselines/reference/controlFlowAliasing.errors.txt @@ -14,10 +14,6 @@ controlFlowAliasing.ts(112,13): error TS2339: Property 'foo' does not exist on t Property 'foo' does not exist on type '{ kind: "bar"; bar: number; }'. controlFlowAliasing.ts(115,13): error TS2339: Property 'bar' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. Property 'bar' does not exist on type '{ kind: "foo"; foo: string; }'. -controlFlowAliasing.ts(134,13): error TS2339: Property 'foo' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. - Property 'foo' does not exist on type '{ kind: "bar"; bar: number; }'. -controlFlowAliasing.ts(137,13): error TS2339: Property 'bar' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. - Property 'bar' does not exist on type '{ kind: "foo"; foo: string; }'. controlFlowAliasing.ts(154,19): error TS2339: Property 'foo' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. Property 'foo' does not exist on type '{ kind: "bar"; bar: number; }'. controlFlowAliasing.ts(157,19): error TS2339: Property 'bar' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. @@ -28,7 +24,7 @@ controlFlowAliasing.ts(280,5): error TS2448: Block-scoped variable 'a' used befo controlFlowAliasing.ts(280,5): error TS2454: Variable 'a' is used before being assigned. -==== controlFlowAliasing.ts (15 errors) ==== +==== controlFlowAliasing.ts (13 errors) ==== // Narrowing by aliased conditional expressions function f10(x: string | number) { @@ -186,16 +182,10 @@ controlFlowAliasing.ts(280,5): error TS2454: Variable 'a' is used before being a let obj = arg; const isFoo = obj.kind === 'foo'; if (isFoo) { - obj.foo; // Not narrowed because obj is mutable - ~~~ -!!! error TS2339: Property 'foo' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. -!!! error TS2339: Property 'foo' does not exist on type '{ kind: "bar"; bar: number; }'. + obj.foo; } else { - obj.bar; // Not narrowed because obj is mutable - ~~~ -!!! error TS2339: Property 'bar' does not exist on type '{ kind: "foo"; foo: string; } | { kind: "bar"; bar: number; }'. -!!! error TS2339: Property 'bar' does not exist on type '{ kind: "foo"; foo: string; }'. + obj.bar; } } diff --git a/tests/baselines/reference/controlFlowAliasing.js b/tests/baselines/reference/controlFlowAliasing.js index de9b8ca84b43f..a37a261e2ac58 100644 --- a/tests/baselines/reference/controlFlowAliasing.js +++ b/tests/baselines/reference/controlFlowAliasing.js @@ -134,10 +134,10 @@ function f25(arg: { kind: 'foo', foo: string } | { kind: 'bar', bar: number }) { let obj = arg; const isFoo = obj.kind === 'foo'; if (isFoo) { - obj.foo; // Not narrowed because obj is mutable + obj.foo; } else { - obj.bar; // Not narrowed because obj is mutable + obj.bar; } } @@ -423,10 +423,10 @@ function f25(arg) { var obj = arg; var isFoo = obj.kind === 'foo'; if (isFoo) { - obj.foo; // Not narrowed because obj is mutable + obj.foo; } else { - obj.bar; // Not narrowed because obj is mutable + obj.bar; } } function f26(outer) { diff --git a/tests/baselines/reference/controlFlowAliasing.symbols b/tests/baselines/reference/controlFlowAliasing.symbols index b0de1519352c4..0fc737c2828d4 100644 --- a/tests/baselines/reference/controlFlowAliasing.symbols +++ b/tests/baselines/reference/controlFlowAliasing.symbols @@ -370,12 +370,16 @@ function f25(arg: { kind: 'foo', foo: string } | { kind: 'bar', bar: number }) { if (isFoo) { >isFoo : Symbol(isFoo, Decl(controlFlowAliasing.ts, 131, 9)) - obj.foo; // Not narrowed because obj is mutable + obj.foo; +>obj.foo : Symbol(foo, Decl(controlFlowAliasing.ts, 129, 32)) >obj : Symbol(obj, Decl(controlFlowAliasing.ts, 130, 7)) +>foo : Symbol(foo, Decl(controlFlowAliasing.ts, 129, 32)) } else { - obj.bar; // Not narrowed because obj is mutable + obj.bar; +>obj.bar : Symbol(bar, Decl(controlFlowAliasing.ts, 129, 63)) >obj : Symbol(obj, Decl(controlFlowAliasing.ts, 130, 7)) +>bar : Symbol(bar, Decl(controlFlowAliasing.ts, 129, 63)) } } diff --git a/tests/baselines/reference/controlFlowAliasing.types b/tests/baselines/reference/controlFlowAliasing.types index b93de24a3aaf9..ea5ee6edf01f3 100644 --- a/tests/baselines/reference/controlFlowAliasing.types +++ b/tests/baselines/reference/controlFlowAliasing.types @@ -440,16 +440,16 @@ function f25(arg: { kind: 'foo', foo: string } | { kind: 'bar', bar: number }) { if (isFoo) { >isFoo : boolean - obj.foo; // Not narrowed because obj is mutable ->obj.foo : any ->obj : { kind: "foo"; foo: string; } | { kind: "bar"; bar: number; } ->foo : any + obj.foo; +>obj.foo : string +>obj : { kind: "foo"; foo: string; } +>foo : string } else { - obj.bar; // Not narrowed because obj is mutable ->obj.bar : any ->obj : { kind: "foo"; foo: string; } | { kind: "bar"; bar: number; } ->bar : any + obj.bar; +>obj.bar : number +>obj : { kind: "bar"; bar: number; } +>bar : number } } diff --git a/tests/baselines/reference/implicitConstParameters.errors.txt b/tests/baselines/reference/implicitConstParameters.errors.txt index ffc18dbc49234..2be671d75de0d 100644 --- a/tests/baselines/reference/implicitConstParameters.errors.txt +++ b/tests/baselines/reference/implicitConstParameters.errors.txt @@ -1,8 +1,7 @@ -implicitConstParameters.ts(38,27): error TS18048: 'x' is possibly 'undefined'. implicitConstParameters.ts(44,27): error TS18048: 'x' is possibly 'undefined'. -==== implicitConstParameters.ts (2 errors) ==== +==== implicitConstParameters.ts (1 errors) ==== function doSomething(cb: () => void) { cb(); } @@ -38,11 +37,9 @@ implicitConstParameters.ts(44,27): error TS18048: 'x' is possibly 'undefined'. } function f4(x: string | undefined) { - x = "abc"; // causes x to be considered non-const + x = "abc"; if (x) { doSomething(() => x.length); - ~ -!!! error TS18048: 'x' is possibly 'undefined'. } } diff --git a/tests/baselines/reference/implicitConstParameters.js b/tests/baselines/reference/implicitConstParameters.js index 001a81d352df8..c14592085405a 100644 --- a/tests/baselines/reference/implicitConstParameters.js +++ b/tests/baselines/reference/implicitConstParameters.js @@ -36,7 +36,7 @@ function f3(x: string | undefined) { } function f4(x: string | undefined) { - x = "abc"; // causes x to be considered non-const + x = "abc"; if (x) { doSomething(() => x.length); } @@ -88,7 +88,7 @@ function f3(x) { } } function f4(x) { - x = "abc"; // causes x to be considered non-const + x = "abc"; if (x) { doSomething(function () { return x.length; }); } diff --git a/tests/baselines/reference/implicitConstParameters.symbols b/tests/baselines/reference/implicitConstParameters.symbols index 298355c44015a..16ba4c949c325 100644 --- a/tests/baselines/reference/implicitConstParameters.symbols +++ b/tests/baselines/reference/implicitConstParameters.symbols @@ -86,7 +86,7 @@ function f4(x: string | undefined) { >f4 : Symbol(f4, Decl(implicitConstParameters.ts, 32, 1)) >x : Symbol(x, Decl(implicitConstParameters.ts, 34, 12)) - x = "abc"; // causes x to be considered non-const + x = "abc"; >x : Symbol(x, Decl(implicitConstParameters.ts, 34, 12)) if (x) { diff --git a/tests/baselines/reference/implicitConstParameters.types b/tests/baselines/reference/implicitConstParameters.types index ef602acb7e11c..4b21f714f817e 100644 --- a/tests/baselines/reference/implicitConstParameters.types +++ b/tests/baselines/reference/implicitConstParameters.types @@ -103,7 +103,7 @@ function f4(x: string | undefined) { >f4 : (x: string | undefined) => void >x : string | undefined - x = "abc"; // causes x to be considered non-const + x = "abc"; >x = "abc" : "abc" >x : string | undefined >"abc" : "abc" @@ -116,7 +116,7 @@ function f4(x: string | undefined) { >doSomething : (cb: () => void) => void >() => x.length : () => number >x.length : number ->x : string | undefined +>x : string >length : number } } diff --git a/tests/baselines/reference/narrowingPastLastAssignment.errors.txt b/tests/baselines/reference/narrowingPastLastAssignment.errors.txt new file mode 100644 index 0000000000000..0477f4c4877a1 --- /dev/null +++ b/tests/baselines/reference/narrowingPastLastAssignment.errors.txt @@ -0,0 +1,163 @@ +narrowingPastLastAssignment.ts(88,9): error TS7034: Variable 'x' implicitly has type 'any' in some locations where its type cannot be determined. +narrowingPastLastAssignment.ts(90,20): error TS7005: Variable 'x' implicitly has an 'any' type. + + +==== narrowingPastLastAssignment.ts (2 errors) ==== + function action(f: Function) {} + + // Narrowings are preserved in closures created past last assignment + + function f1(x: string | number) { + x = "abc"; + action(() => { x /* string | number */ }); + x = 42; + action(() => { x /* number */ }); + } + + // Narrowings are not preserved in inner function and class declarations (due to hoisting) + + function f2() { + let x: string | number; + x = 42; + let a = () => { x /* number */ }; + let f = function() { x /* number */ }; + let C = class { + foo() { x /* number */ } + }; + let o = { + foo() { x /* number */ } + }; + function g() { x /* string | number */ } + class A { + foo() { x /* string | number */ } + } + } + + // Narrowings are not preserved when assignments occur in inner functions + + function f3(x: string | number) { + action(() => { x = "abc" }); + x = 42; + action(() => { x /* string | number */ }); + } + + // Assignment effects in compoud statements extend to the entire statement + + function f4(cond: () => boolean) { + let x: string | number = 0; + while (cond()) { + x = "abc"; + action(() => { x /* string | number */ }); + x = 42; + action(() => { x /* string | number */ }); + } + action(() => { x /* number */ }); + } + + function f5(x: string | number, cond: () => boolean) { + if (cond()) { + x = 1; + action(() => { x /* string | number */ }); + } + else { + x = 2; + action(() => { x /* string | number */ }); + } + action(() => { x /* number */ }); + } + + function f5a(cond: boolean) { + if (cond) { + let x: number | undefined; + x = 1; + action(() => { x /* number */ }); + } + else { + let x: number | undefined; + x = 2; + action(() => { x /* number */ }); + } + } + + function f5b() { + for (let x = 0; x < 10; x++) { + if (x === 1 || x === 2) { + action(() => { x /* 1 | 2 */ }) + } + } + } + + // Implicit any variables have a known type following last assignment + + function f6() { + let x; + ~ +!!! error TS7034: Variable 'x' implicitly has type 'any' in some locations where its type cannot be determined. + x = "abc"; + action(() => { x }); // Error + ~ +!!! error TS7005: Variable 'x' implicitly has an 'any' type. + x = 42; + action(() => { x /* number */ }); + } + + // Narrowings on catch variables are preserved past last assignment + + function f7() { + try { + } + catch (e) { + if (e instanceof Error) { + let f = () => { e /* Error */ } + } + } + } + + // Narrowings are not preserved for global variables + + let g: string | number; + g = "abc"; + action(() => { g /* string | number */ }); + + // Narrowings are not preserved for exported namespace members + + namespace Foo { + export let x: string | number; + x = "abc"; + action(() => { x /* string | number */ }); + let y: string | number; + y = "abc"; + action(() => { y /* string */ }); + } + + // Repros from #35124 + + function f10() { + let i: number | undefined; + i = 0; + return (k: number) => k === i + 1; + } + + function makeAdder(n?: number) { + n ??= 0; + return (m: number) => n + m; + } + + function f11() { + let r; + r = "b"; + () => r; + } + + // Repro from #52104 + + function f12() { + const fooMap: Map> = new Map() + const values = [1, 2, 3, 4, 5]; + let foo = fooMap.get("a"); + if (foo == null) { + foo = []; + } + values.forEach(v => foo.push(v)); + } + \ No newline at end of file diff --git a/tests/baselines/reference/narrowingPastLastAssignment.symbols b/tests/baselines/reference/narrowingPastLastAssignment.symbols new file mode 100644 index 0000000000000..4ace603d16418 --- /dev/null +++ b/tests/baselines/reference/narrowingPastLastAssignment.symbols @@ -0,0 +1,365 @@ +//// [tests/cases/compiler/narrowingPastLastAssignment.ts] //// + +=== narrowingPastLastAssignment.ts === +function action(f: Function) {} +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>f : Symbol(f, Decl(narrowingPastLastAssignment.ts, 0, 16)) +>Function : Symbol(Function, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.esnext.decorators.d.ts, --, --)) + +// Narrowings are preserved in closures created past last assignment + +function f1(x: string | number) { +>f1 : Symbol(f1, Decl(narrowingPastLastAssignment.ts, 0, 31)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 4, 12)) + + x = "abc"; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 4, 12)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 4, 12)) + + x = 42; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 4, 12)) + + action(() => { x /* number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 4, 12)) +} + +// Narrowings are not preserved in inner function and class declarations (due to hoisting) + +function f2() { +>f2 : Symbol(f2, Decl(narrowingPastLastAssignment.ts, 9, 1)) + + let x: string | number; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + x = 42; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + let a = () => { x /* number */ }; +>a : Symbol(a, Decl(narrowingPastLastAssignment.ts, 16, 7)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + let f = function() { x /* number */ }; +>f : Symbol(f, Decl(narrowingPastLastAssignment.ts, 17, 7)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + let C = class { +>C : Symbol(C, Decl(narrowingPastLastAssignment.ts, 18, 7)) + + foo() { x /* number */ } +>foo : Symbol(C.foo, Decl(narrowingPastLastAssignment.ts, 18, 19)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + }; + let o = { +>o : Symbol(o, Decl(narrowingPastLastAssignment.ts, 21, 7)) + + foo() { x /* number */ } +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 21, 13)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + }; + function g() { x /* string | number */ } +>g : Symbol(g, Decl(narrowingPastLastAssignment.ts, 23, 6)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + + class A { +>A : Symbol(A, Decl(narrowingPastLastAssignment.ts, 24, 44)) + + foo() { x /* string | number */ } +>foo : Symbol(A.foo, Decl(narrowingPastLastAssignment.ts, 25, 13)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 14, 7)) + } +} + +// Narrowings are not preserved when assignments occur in inner functions + +function f3(x: string | number) { +>f3 : Symbol(f3, Decl(narrowingPastLastAssignment.ts, 28, 1)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 32, 12)) + + action(() => { x = "abc" }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 32, 12)) + + x = 42; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 32, 12)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 32, 12)) +} + +// Assignment effects in compoud statements extend to the entire statement + +function f4(cond: () => boolean) { +>f4 : Symbol(f4, Decl(narrowingPastLastAssignment.ts, 36, 1)) +>cond : Symbol(cond, Decl(narrowingPastLastAssignment.ts, 40, 12)) + + let x: string | number = 0; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 41, 7)) + + while (cond()) { +>cond : Symbol(cond, Decl(narrowingPastLastAssignment.ts, 40, 12)) + + x = "abc"; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 41, 7)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 41, 7)) + + x = 42; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 41, 7)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 41, 7)) + } + action(() => { x /* number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 41, 7)) +} + +function f5(x: string | number, cond: () => boolean) { +>f5 : Symbol(f5, Decl(narrowingPastLastAssignment.ts, 49, 1)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 51, 12)) +>cond : Symbol(cond, Decl(narrowingPastLastAssignment.ts, 51, 31)) + + if (cond()) { +>cond : Symbol(cond, Decl(narrowingPastLastAssignment.ts, 51, 31)) + + x = 1; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 51, 12)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 51, 12)) + } + else { + x = 2; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 51, 12)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 51, 12)) + } + action(() => { x /* number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 51, 12)) +} + +function f5a(cond: boolean) { +>f5a : Symbol(f5a, Decl(narrowingPastLastAssignment.ts, 61, 1)) +>cond : Symbol(cond, Decl(narrowingPastLastAssignment.ts, 63, 13)) + + if (cond) { +>cond : Symbol(cond, Decl(narrowingPastLastAssignment.ts, 63, 13)) + + let x: number | undefined; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 65, 11)) + + x = 1; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 65, 11)) + + action(() => { x /* number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 65, 11)) + } + else { + let x: number | undefined; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 70, 11)) + + x = 2; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 70, 11)) + + action(() => { x /* number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 70, 11)) + } +} + +function f5b() { +>f5b : Symbol(f5b, Decl(narrowingPastLastAssignment.ts, 74, 1)) + + for (let x = 0; x < 10; x++) { +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 77, 12)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 77, 12)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 77, 12)) + + if (x === 1 || x === 2) { +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 77, 12)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 77, 12)) + + action(() => { x /* 1 | 2 */ }) +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 77, 12)) + } + } +} + +// Implicit any variables have a known type following last assignment + +function f6() { +>f6 : Symbol(f6, Decl(narrowingPastLastAssignment.ts, 82, 1)) + + let x; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 87, 7)) + + x = "abc"; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 87, 7)) + + action(() => { x }); // Error +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 87, 7)) + + x = 42; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 87, 7)) + + action(() => { x /* number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 87, 7)) +} + +// Narrowings on catch variables are preserved past last assignment + +function f7() { +>f7 : Symbol(f7, Decl(narrowingPastLastAssignment.ts, 92, 1)) + + try { + } + catch (e) { +>e : Symbol(e, Decl(narrowingPastLastAssignment.ts, 99, 11)) + + if (e instanceof Error) { +>e : Symbol(e, Decl(narrowingPastLastAssignment.ts, 99, 11)) +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2022.error.d.ts, --, --)) + + let f = () => { e /* Error */ } +>f : Symbol(f, Decl(narrowingPastLastAssignment.ts, 101, 15)) +>e : Symbol(e, Decl(narrowingPastLastAssignment.ts, 99, 11)) + } + } +} + +// Narrowings are not preserved for global variables + +let g: string | number; +>g : Symbol(g, Decl(narrowingPastLastAssignment.ts, 108, 3)) + +g = "abc"; +>g : Symbol(g, Decl(narrowingPastLastAssignment.ts, 108, 3)) + +action(() => { g /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>g : Symbol(g, Decl(narrowingPastLastAssignment.ts, 108, 3)) + +// Narrowings are not preserved for exported namespace members + +namespace Foo { +>Foo : Symbol(Foo, Decl(narrowingPastLastAssignment.ts, 110, 42)) + + export let x: string | number; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 115, 14)) + + x = "abc"; +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 115, 14)) + + action(() => { x /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>x : Symbol(x, Decl(narrowingPastLastAssignment.ts, 115, 14)) + + let y: string | number; +>y : Symbol(y, Decl(narrowingPastLastAssignment.ts, 118, 7)) + + y = "abc"; +>y : Symbol(y, Decl(narrowingPastLastAssignment.ts, 118, 7)) + + action(() => { y /* string */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignment.ts, 0, 0)) +>y : Symbol(y, Decl(narrowingPastLastAssignment.ts, 118, 7)) +} + +// Repros from #35124 + +function f10() { +>f10 : Symbol(f10, Decl(narrowingPastLastAssignment.ts, 121, 1)) + + let i: number | undefined; +>i : Symbol(i, Decl(narrowingPastLastAssignment.ts, 126, 7)) + + i = 0; +>i : Symbol(i, Decl(narrowingPastLastAssignment.ts, 126, 7)) + + return (k: number) => k === i + 1; +>k : Symbol(k, Decl(narrowingPastLastAssignment.ts, 128, 12)) +>k : Symbol(k, Decl(narrowingPastLastAssignment.ts, 128, 12)) +>i : Symbol(i, Decl(narrowingPastLastAssignment.ts, 126, 7)) +} + +function makeAdder(n?: number) { +>makeAdder : Symbol(makeAdder, Decl(narrowingPastLastAssignment.ts, 129, 1)) +>n : Symbol(n, Decl(narrowingPastLastAssignment.ts, 131, 19)) + + n ??= 0; +>n : Symbol(n, Decl(narrowingPastLastAssignment.ts, 131, 19)) + + return (m: number) => n + m; +>m : Symbol(m, Decl(narrowingPastLastAssignment.ts, 133, 12)) +>n : Symbol(n, Decl(narrowingPastLastAssignment.ts, 131, 19)) +>m : Symbol(m, Decl(narrowingPastLastAssignment.ts, 133, 12)) +} + +function f11() { +>f11 : Symbol(f11, Decl(narrowingPastLastAssignment.ts, 134, 1)) + + let r; +>r : Symbol(r, Decl(narrowingPastLastAssignment.ts, 137, 7)) + + r = "b"; +>r : Symbol(r, Decl(narrowingPastLastAssignment.ts, 137, 7)) + + () => r; +>r : Symbol(r, Decl(narrowingPastLastAssignment.ts, 137, 7)) +} + +// Repro from #52104 + +function f12() { +>f12 : Symbol(f12, Decl(narrowingPastLastAssignment.ts, 140, 1)) + + const fooMap: Map> = new Map() +>fooMap : Symbol(fooMap, Decl(narrowingPastLastAssignment.ts, 145, 9)) +>Map : Symbol(Map, Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --)) +>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --) ... and 4 more) +>Map : Symbol(Map, Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.collection.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --)) + + const values = [1, 2, 3, 4, 5]; +>values : Symbol(values, Decl(narrowingPastLastAssignment.ts, 146, 9)) + + let foo = fooMap.get("a"); +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 147, 7)) +>fooMap.get : Symbol(Map.get, Decl(lib.es2015.collection.d.ts, --, --)) +>fooMap : Symbol(fooMap, Decl(narrowingPastLastAssignment.ts, 145, 9)) +>get : Symbol(Map.get, Decl(lib.es2015.collection.d.ts, --, --)) + + if (foo == null) { +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 147, 7)) + + foo = []; +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 147, 7)) + } + values.forEach(v => foo.push(v)); +>values.forEach : Symbol(Array.forEach, Decl(lib.es5.d.ts, --, --)) +>values : Symbol(values, Decl(narrowingPastLastAssignment.ts, 146, 9)) +>forEach : Symbol(Array.forEach, Decl(lib.es5.d.ts, --, --)) +>v : Symbol(v, Decl(narrowingPastLastAssignment.ts, 151, 19)) +>foo.push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --)) +>foo : Symbol(foo, Decl(narrowingPastLastAssignment.ts, 147, 7)) +>push : Symbol(Array.push, Decl(lib.es5.d.ts, --, --)) +>v : Symbol(v, Decl(narrowingPastLastAssignment.ts, 151, 19)) +} + diff --git a/tests/baselines/reference/narrowingPastLastAssignment.types b/tests/baselines/reference/narrowingPastLastAssignment.types new file mode 100644 index 0000000000000..18dce42399162 --- /dev/null +++ b/tests/baselines/reference/narrowingPastLastAssignment.types @@ -0,0 +1,476 @@ +//// [tests/cases/compiler/narrowingPastLastAssignment.ts] //// + +=== narrowingPastLastAssignment.ts === +function action(f: Function) {} +>action : (f: Function) => void +>f : Function + +// Narrowings are preserved in closures created past last assignment + +function f1(x: string | number) { +>f1 : (x: string | number) => void +>x : string | number + + x = "abc"; +>x = "abc" : "abc" +>x : string | number +>"abc" : "abc" + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number + + x = 42; +>x = 42 : 42 +>x : string | number +>42 : 42 + + action(() => { x /* number */ }); +>action(() => { x /* number */ }) : void +>action : (f: Function) => void +>() => { x /* number */ } : () => void +>x : number +} + +// Narrowings are not preserved in inner function and class declarations (due to hoisting) + +function f2() { +>f2 : () => void + + let x: string | number; +>x : string | number + + x = 42; +>x = 42 : 42 +>x : string | number +>42 : 42 + + let a = () => { x /* number */ }; +>a : () => void +>() => { x /* number */ } : () => void +>x : number + + let f = function() { x /* number */ }; +>f : () => void +>function() { x /* number */ } : () => void +>x : number + + let C = class { +>C : typeof C +>class { foo() { x /* number */ } } : typeof C + + foo() { x /* number */ } +>foo : () => void +>x : number + + }; + let o = { +>o : { foo(): void; } +>{ foo() { x /* number */ } } : { foo(): void; } + + foo() { x /* number */ } +>foo : () => void +>x : number + + }; + function g() { x /* string | number */ } +>g : () => void +>x : string | number + + class A { +>A : A + + foo() { x /* string | number */ } +>foo : () => void +>x : string | number + } +} + +// Narrowings are not preserved when assignments occur in inner functions + +function f3(x: string | number) { +>f3 : (x: string | number) => void +>x : string | number + + action(() => { x = "abc" }); +>action(() => { x = "abc" }) : void +>action : (f: Function) => void +>() => { x = "abc" } : () => void +>x = "abc" : "abc" +>x : string | number +>"abc" : "abc" + + x = 42; +>x = 42 : 42 +>x : string | number +>42 : 42 + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number +} + +// Assignment effects in compoud statements extend to the entire statement + +function f4(cond: () => boolean) { +>f4 : (cond: () => boolean) => void +>cond : () => boolean + + let x: string | number = 0; +>x : string | number +>0 : 0 + + while (cond()) { +>cond() : boolean +>cond : () => boolean + + x = "abc"; +>x = "abc" : "abc" +>x : string | number +>"abc" : "abc" + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number + + x = 42; +>x = 42 : 42 +>x : string | number +>42 : 42 + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number + } + action(() => { x /* number */ }); +>action(() => { x /* number */ }) : void +>action : (f: Function) => void +>() => { x /* number */ } : () => void +>x : number +} + +function f5(x: string | number, cond: () => boolean) { +>f5 : (x: string | number, cond: () => boolean) => void +>x : string | number +>cond : () => boolean + + if (cond()) { +>cond() : boolean +>cond : () => boolean + + x = 1; +>x = 1 : 1 +>x : string | number +>1 : 1 + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number + } + else { + x = 2; +>x = 2 : 2 +>x : string | number +>2 : 2 + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number + } + action(() => { x /* number */ }); +>action(() => { x /* number */ }) : void +>action : (f: Function) => void +>() => { x /* number */ } : () => void +>x : number +} + +function f5a(cond: boolean) { +>f5a : (cond: boolean) => void +>cond : boolean + + if (cond) { +>cond : boolean + + let x: number | undefined; +>x : number | undefined + + x = 1; +>x = 1 : 1 +>x : number | undefined +>1 : 1 + + action(() => { x /* number */ }); +>action(() => { x /* number */ }) : void +>action : (f: Function) => void +>() => { x /* number */ } : () => void +>x : number + } + else { + let x: number | undefined; +>x : number | undefined + + x = 2; +>x = 2 : 2 +>x : number | undefined +>2 : 2 + + action(() => { x /* number */ }); +>action(() => { x /* number */ }) : void +>action : (f: Function) => void +>() => { x /* number */ } : () => void +>x : number + } +} + +function f5b() { +>f5b : () => void + + for (let x = 0; x < 10; x++) { +>x : number +>0 : 0 +>x < 10 : boolean +>x : number +>10 : 10 +>x++ : number +>x : number + + if (x === 1 || x === 2) { +>x === 1 || x === 2 : boolean +>x === 1 : boolean +>x : number +>1 : 1 +>x === 2 : boolean +>x : number +>2 : 2 + + action(() => { x /* 1 | 2 */ }) +>action(() => { x /* 1 | 2 */ }) : void +>action : (f: Function) => void +>() => { x /* 1 | 2 */ } : () => void +>x : 1 | 2 + } + } +} + +// Implicit any variables have a known type following last assignment + +function f6() { +>f6 : () => void + + let x; +>x : any + + x = "abc"; +>x = "abc" : "abc" +>x : any +>"abc" : "abc" + + action(() => { x }); // Error +>action(() => { x }) : void +>action : (f: Function) => void +>() => { x } : () => void +>x : any + + x = 42; +>x = 42 : 42 +>x : any +>42 : 42 + + action(() => { x /* number */ }); +>action(() => { x /* number */ }) : void +>action : (f: Function) => void +>() => { x /* number */ } : () => void +>x : number +} + +// Narrowings on catch variables are preserved past last assignment + +function f7() { +>f7 : () => void + + try { + } + catch (e) { +>e : unknown + + if (e instanceof Error) { +>e instanceof Error : boolean +>e : unknown +>Error : ErrorConstructor + + let f = () => { e /* Error */ } +>f : () => void +>() => { e /* Error */ } : () => void +>e : Error + } + } +} + +// Narrowings are not preserved for global variables + +let g: string | number; +>g : string | number + +g = "abc"; +>g = "abc" : "abc" +>g : string | number +>"abc" : "abc" + +action(() => { g /* string | number */ }); +>action(() => { g /* string | number */ }) : void +>action : (f: Function) => void +>() => { g /* string | number */ } : () => void +>g : string | number + +// Narrowings are not preserved for exported namespace members + +namespace Foo { +>Foo : typeof Foo + + export let x: string | number; +>x : string | number + + x = "abc"; +>x = "abc" : "abc" +>x : string | number +>"abc" : "abc" + + action(() => { x /* string | number */ }); +>action(() => { x /* string | number */ }) : void +>action : (f: Function) => void +>() => { x /* string | number */ } : () => void +>x : string | number + + let y: string | number; +>y : string | number + + y = "abc"; +>y = "abc" : "abc" +>y : string | number +>"abc" : "abc" + + action(() => { y /* string */ }); +>action(() => { y /* string */ }) : void +>action : (f: Function) => void +>() => { y /* string */ } : () => void +>y : string +} + +// Repros from #35124 + +function f10() { +>f10 : () => (k: number) => boolean + + let i: number | undefined; +>i : number | undefined + + i = 0; +>i = 0 : 0 +>i : number | undefined +>0 : 0 + + return (k: number) => k === i + 1; +>(k: number) => k === i + 1 : (k: number) => boolean +>k : number +>k === i + 1 : boolean +>k : number +>i + 1 : number +>i : number +>1 : 1 +} + +function makeAdder(n?: number) { +>makeAdder : (n?: number) => (m: number) => number +>n : number | undefined + + n ??= 0; +>n ??= 0 : number +>n : number | undefined +>0 : 0 + + return (m: number) => n + m; +>(m: number) => n + m : (m: number) => number +>m : number +>n + m : number +>n : number +>m : number +} + +function f11() { +>f11 : () => void + + let r; +>r : any + + r = "b"; +>r = "b" : "b" +>r : any +>"b" : "b" + + () => r; +>() => r : () => string +>r : string +} + +// Repro from #52104 + +function f12() { +>f12 : () => void + + const fooMap: Map> = new Map() +>fooMap : Map +>new Map() : Map +>Map : MapConstructor + + const values = [1, 2, 3, 4, 5]; +>values : number[] +>[1, 2, 3, 4, 5] : number[] +>1 : 1 +>2 : 2 +>3 : 3 +>4 : 4 +>5 : 5 + + let foo = fooMap.get("a"); +>foo : number[] | undefined +>fooMap.get("a") : number[] | undefined +>fooMap.get : (key: string) => number[] | undefined +>fooMap : Map +>get : (key: string) => number[] | undefined +>"a" : "a" + + if (foo == null) { +>foo == null : boolean +>foo : number[] | undefined + + foo = []; +>foo = [] : never[] +>foo : number[] | undefined +>[] : never[] + } + values.forEach(v => foo.push(v)); +>values.forEach(v => foo.push(v)) : void +>values.forEach : (callbackfn: (value: number, index: number, array: number[]) => void, thisArg?: any) => void +>values : number[] +>forEach : (callbackfn: (value: number, index: number, array: number[]) => void, thisArg?: any) => void +>v => foo.push(v) : (v: number) => number +>v : number +>foo.push(v) : number +>foo.push : (...items: number[]) => number +>foo : number[] +>push : (...items: number[]) => number +>v : number +} + diff --git a/tests/baselines/reference/narrowingPastLastAssignmentInModule.symbols b/tests/baselines/reference/narrowingPastLastAssignmentInModule.symbols new file mode 100644 index 0000000000000..c4b5b29a98b81 --- /dev/null +++ b/tests/baselines/reference/narrowingPastLastAssignmentInModule.symbols @@ -0,0 +1,70 @@ +//// [tests/cases/compiler/narrowingPastLastAssignmentInModule.ts] //// + +=== narrowingPastLastAssignmentInModule.ts === +function action(f: Function) {} +>action : Symbol(action, Decl(narrowingPastLastAssignmentInModule.ts, 0, 0)) +>f : Symbol(f, Decl(narrowingPastLastAssignmentInModule.ts, 0, 16)) +>Function : Symbol(Function, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.esnext.decorators.d.ts, --, --)) + +// Narrowings are not preserved for exported mutable variables + +export let x1: string | number; +>x1 : Symbol(x1, Decl(narrowingPastLastAssignmentInModule.ts, 4, 10)) + +x1 = "abc"; +>x1 : Symbol(x1, Decl(narrowingPastLastAssignmentInModule.ts, 4, 10)) + +action(() => { x1 /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignmentInModule.ts, 0, 0)) +>x1 : Symbol(x1, Decl(narrowingPastLastAssignmentInModule.ts, 4, 10)) + +export { x2 }; +>x2 : Symbol(x2, Decl(narrowingPastLastAssignmentInModule.ts, 8, 8)) + +let x2: string | number; +>x2 : Symbol(x2, Decl(narrowingPastLastAssignmentInModule.ts, 9, 3)) + +x2 = "abc"; +>x2 : Symbol(x2, Decl(narrowingPastLastAssignmentInModule.ts, 9, 3)) + +action(() => { x2 /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignmentInModule.ts, 0, 0)) +>x2 : Symbol(x2, Decl(narrowingPastLastAssignmentInModule.ts, 9, 3)) + +export { x3 as foo }; +>x3 : Symbol(x3, Decl(narrowingPastLastAssignmentInModule.ts, 14, 3)) +>foo : Symbol(foo, Decl(narrowingPastLastAssignmentInModule.ts, 13, 8)) + +let x3: string | number; +>x3 : Symbol(x3, Decl(narrowingPastLastAssignmentInModule.ts, 14, 3)) + +x3 = "abc"; +>x3 : Symbol(x3, Decl(narrowingPastLastAssignmentInModule.ts, 14, 3)) + +action(() => { x3 /* string | number */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignmentInModule.ts, 0, 0)) +>x3 : Symbol(x3, Decl(narrowingPastLastAssignmentInModule.ts, 14, 3)) + +let x4: string | number; +>x4 : Symbol(x4, Decl(narrowingPastLastAssignmentInModule.ts, 18, 3)) + +x4 = "abc"; +>x4 : Symbol(x4, Decl(narrowingPastLastAssignmentInModule.ts, 18, 3)) + +action(() => { x4 /* string */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignmentInModule.ts, 0, 0)) +>x4 : Symbol(x4, Decl(narrowingPastLastAssignmentInModule.ts, 18, 3)) + +export default x4; +>x4 : Symbol(x4, Decl(narrowingPastLastAssignmentInModule.ts, 18, 3)) + +let x5: string | number; +>x5 : Symbol(x5, Decl(narrowingPastLastAssignmentInModule.ts, 23, 3)) + +x5 = "abc"; +>x5 : Symbol(x5, Decl(narrowingPastLastAssignmentInModule.ts, 23, 3)) + +action(() => { x5 /* string */ }); +>action : Symbol(action, Decl(narrowingPastLastAssignmentInModule.ts, 0, 0)) +>x5 : Symbol(x5, Decl(narrowingPastLastAssignmentInModule.ts, 23, 3)) + diff --git a/tests/baselines/reference/narrowingPastLastAssignmentInModule.types b/tests/baselines/reference/narrowingPastLastAssignmentInModule.types new file mode 100644 index 0000000000000..ccb173341e07a --- /dev/null +++ b/tests/baselines/reference/narrowingPastLastAssignmentInModule.types @@ -0,0 +1,89 @@ +//// [tests/cases/compiler/narrowingPastLastAssignmentInModule.ts] //// + +=== narrowingPastLastAssignmentInModule.ts === +function action(f: Function) {} +>action : (f: Function) => void +>f : Function + +// Narrowings are not preserved for exported mutable variables + +export let x1: string | number; +>x1 : string | number + +x1 = "abc"; +>x1 = "abc" : "abc" +>x1 : string | number +>"abc" : "abc" + +action(() => { x1 /* string | number */ }); +>action(() => { x1 /* string | number */ }) : void +>action : (f: Function) => void +>() => { x1 /* string | number */ } : () => void +>x1 : string | number + +export { x2 }; +>x2 : string | number + +let x2: string | number; +>x2 : string | number + +x2 = "abc"; +>x2 = "abc" : "abc" +>x2 : string | number +>"abc" : "abc" + +action(() => { x2 /* string | number */ }); +>action(() => { x2 /* string | number */ }) : void +>action : (f: Function) => void +>() => { x2 /* string | number */ } : () => void +>x2 : string | number + +export { x3 as foo }; +>x3 : string | number +>foo : string | number + +let x3: string | number; +>x3 : string | number + +x3 = "abc"; +>x3 = "abc" : "abc" +>x3 : string | number +>"abc" : "abc" + +action(() => { x3 /* string | number */ }); +>action(() => { x3 /* string | number */ }) : void +>action : (f: Function) => void +>() => { x3 /* string | number */ } : () => void +>x3 : string | number + +let x4: string | number; +>x4 : string | number + +x4 = "abc"; +>x4 = "abc" : "abc" +>x4 : string | number +>"abc" : "abc" + +action(() => { x4 /* string */ }); +>action(() => { x4 /* string */ }) : void +>action : (f: Function) => void +>() => { x4 /* string */ } : () => void +>x4 : string + +export default x4; +>x4 : string | number + +let x5: string | number; +>x5 : string | number + +x5 = "abc"; +>x5 = "abc" : "abc" +>x5 : string | number +>"abc" : "abc" + +action(() => { x5 /* string */ }); +>action(() => { x5 /* string */ }) : void +>action : (f: Function) => void +>() => { x5 /* string */ } : () => void +>x5 : string + diff --git a/tests/cases/compiler/implicitConstParameters.ts b/tests/cases/compiler/implicitConstParameters.ts index 97996789124f6..f5c2cd5e79828 100644 --- a/tests/cases/compiler/implicitConstParameters.ts +++ b/tests/cases/compiler/implicitConstParameters.ts @@ -35,7 +35,7 @@ function f3(x: string | undefined) { } function f4(x: string | undefined) { - x = "abc"; // causes x to be considered non-const + x = "abc"; if (x) { doSomething(() => x.length); } diff --git a/tests/cases/compiler/narrowingPastLastAssignment.ts b/tests/cases/compiler/narrowingPastLastAssignment.ts new file mode 100644 index 0000000000000..ae66ab03db719 --- /dev/null +++ b/tests/cases/compiler/narrowingPastLastAssignment.ts @@ -0,0 +1,157 @@ +// @strict: true +// @noEmit: true +// @target: esnext + +function action(f: Function) {} + +// Narrowings are preserved in closures created past last assignment + +function f1(x: string | number) { + x = "abc"; + action(() => { x /* string | number */ }); + x = 42; + action(() => { x /* number */ }); +} + +// Narrowings are not preserved in inner function and class declarations (due to hoisting) + +function f2() { + let x: string | number; + x = 42; + let a = () => { x /* number */ }; + let f = function() { x /* number */ }; + let C = class { + foo() { x /* number */ } + }; + let o = { + foo() { x /* number */ } + }; + function g() { x /* string | number */ } + class A { + foo() { x /* string | number */ } + } +} + +// Narrowings are not preserved when assignments occur in inner functions + +function f3(x: string | number) { + action(() => { x = "abc" }); + x = 42; + action(() => { x /* string | number */ }); +} + +// Assignment effects in compoud statements extend to the entire statement + +function f4(cond: () => boolean) { + let x: string | number = 0; + while (cond()) { + x = "abc"; + action(() => { x /* string | number */ }); + x = 42; + action(() => { x /* string | number */ }); + } + action(() => { x /* number */ }); +} + +function f5(x: string | number, cond: () => boolean) { + if (cond()) { + x = 1; + action(() => { x /* string | number */ }); + } + else { + x = 2; + action(() => { x /* string | number */ }); + } + action(() => { x /* number */ }); +} + +function f5a(cond: boolean) { + if (cond) { + let x: number | undefined; + x = 1; + action(() => { x /* number */ }); + } + else { + let x: number | undefined; + x = 2; + action(() => { x /* number */ }); + } +} + +function f5b() { + for (let x = 0; x < 10; x++) { + if (x === 1 || x === 2) { + action(() => { x /* 1 | 2 */ }) + } + } +} + +// Implicit any variables have a known type following last assignment + +function f6() { + let x; + x = "abc"; + action(() => { x }); // Error + x = 42; + action(() => { x /* number */ }); +} + +// Narrowings on catch variables are preserved past last assignment + +function f7() { + try { + } + catch (e) { + if (e instanceof Error) { + let f = () => { e /* Error */ } + } + } +} + +// Narrowings are not preserved for global variables + +let g: string | number; +g = "abc"; +action(() => { g /* string | number */ }); + +// Narrowings are not preserved for exported namespace members + +namespace Foo { + export let x: string | number; + x = "abc"; + action(() => { x /* string | number */ }); + let y: string | number; + y = "abc"; + action(() => { y /* string */ }); +} + +// Repros from #35124 + +function f10() { + let i: number | undefined; + i = 0; + return (k: number) => k === i + 1; +} + +function makeAdder(n?: number) { + n ??= 0; + return (m: number) => n + m; +} + +function f11() { + let r; + r = "b"; + () => r; +} + +// Repro from #52104 + +function f12() { + const fooMap: Map> = new Map() + const values = [1, 2, 3, 4, 5]; + let foo = fooMap.get("a"); + if (foo == null) { + foo = []; + } + values.forEach(v => foo.push(v)); +} diff --git a/tests/cases/compiler/narrowingPastLastAssignmentInModule.ts b/tests/cases/compiler/narrowingPastLastAssignmentInModule.ts new file mode 100644 index 0000000000000..b036091fc851a --- /dev/null +++ b/tests/cases/compiler/narrowingPastLastAssignmentInModule.ts @@ -0,0 +1,30 @@ +// @strict: true +// @noEmit: true +// @target: esnext + +function action(f: Function) {} + +// Narrowings are not preserved for exported mutable variables + +export let x1: string | number; +x1 = "abc"; +action(() => { x1 /* string | number */ }); + +export { x2 }; +let x2: string | number; +x2 = "abc"; +action(() => { x2 /* string | number */ }); + +export { x3 as foo }; +let x3: string | number; +x3 = "abc"; +action(() => { x3 /* string | number */ }); + +let x4: string | number; +x4 = "abc"; +action(() => { x4 /* string */ }); +export default x4; + +let x5: string | number; +x5 = "abc"; +action(() => { x5 /* string */ }); diff --git a/tests/cases/conformance/controlFlow/controlFlowAliasing.ts b/tests/cases/conformance/controlFlow/controlFlowAliasing.ts index b32f80abae94f..f9a3defe79bf9 100644 --- a/tests/cases/conformance/controlFlow/controlFlowAliasing.ts +++ b/tests/cases/conformance/controlFlow/controlFlowAliasing.ts @@ -134,10 +134,10 @@ function f25(arg: { kind: 'foo', foo: string } | { kind: 'bar', bar: number }) { let obj = arg; const isFoo = obj.kind === 'foo'; if (isFoo) { - obj.foo; // Not narrowed because obj is mutable + obj.foo; } else { - obj.bar; // Not narrowed because obj is mutable + obj.bar; } }