Preserve type refinements in closures created past last assignment (#56908)

This commit is contained in:
Anders Hejlsberg 2024-01-08 18:21:05 -10:00 committed by GitHub
parent f9cb96c03d
commit 8ff77fbc48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1485 additions and 68 deletions

View File

@ -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;
}
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;
}
else {
forEachChild(node, markNodeAssignments);
}
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);
}

View File

@ -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<number, Declaration>; // detected late-bound assignment declarations associated with the symbol
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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))
}
}

View File

@ -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
}
}

View File

@ -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'.
}
}

View File

@ -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; });
}

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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<string,Array<number>> = new Map()
const values = [1, 2, 3, 4, 5];
let foo = fooMap.get("a");
if (foo == null) {
foo = [];
}
values.forEach(v => foo.push(v));
}

View File

@ -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<string,Array<number>> = 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))
}

View File

@ -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<string,Array<number>> = new Map()
>fooMap : Map<string, number[]>
>new Map() : Map<any, any>
>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<string, number[]>
>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
}

View File

@ -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))

View File

@ -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

View File

@ -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);
}

View File

@ -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<string,Array<number>> = new Map()
const values = [1, 2, 3, 4, 5];
let foo = fooMap.get("a");
if (foo == null) {
foo = [];
}
values.forEach(v => foo.push(v));
}

View File

@ -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 */ });

View File

@ -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;
}
}