Preserve type refinements in closures created past last assignment by ahejlsberg · Pull Request #56908 · microsoft/TypeScript (original) (raw)
We currently preserve type refinements in closures for const
variables and parameters that are never targeted in assignments. With this PR we also preserve type refinements for parameters, local let
variables, and catch
clause variables in closures that are created past the last assignment to those parameters or variables. For example:
declare function action(cb: () => void): void;
function f1() { let x: string | number; x = "abc"; action(() => { x }); // x has type string | number x = 42; action(() => { x }); // x has type number }
Above, it is unknown when (or even if) the action
function will invoke a callback function previously passed to it. Thus, when the first arrow function is created, it is unknown whether it will be invoked with x
having type string
or number
because there are subsequent assignments to x
. However, the second arrow function is created past the last assignment to x
, so x
can safely be narrowed to type number
in that arrow function.
A similar example that adjusts an argument value before using it in a closure:
function makeAdder(n?: number) { n ??= 0; return (m: number) => n + m; // Now ok, previously was error }
Type refinements 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 */ } } }
Implicit any
variables have a known type following the last assignment:
function f3() { let x; x = "abc"; action(() => { x }); // Implicit any error x = 42; action(() => { x /* number */ }); }
Type refinements for catch
variables are preserved past the last assignment (if any):
function f4() { try { } catch (e) { if (e instanceof Error) { let f = () => { e /* Error */ } } } }
Note that effects of assignments in compound statements extend to the entire statement:
function f5(cond: boolean) { let x: number | undefined; if (cond) { x = 1; action(() => { x /* number | undefined / }); } else { x = 2; action(() => { x / number | undefined / }); } action(() => { x / number */ }); }
Above, the type of x
is narrowed to number
only in the last arrow function, even though it would be safe to narrow in the other two arrow functions. That, however, would require full control flow analysis which is significantly more complex and expensive than the single pass we do now.
This PR fixes multiple issues that have previously been attributed to #9998.
Fixes #13142.
Fixes #13560.
Fixes #13572.
Fixes #14748.
Fixes #16285.
Fixes #17240.
Fixes #17449.
Fixes #19606.
Fixes #19683.
Fixes #19698.
Fixes #19918.
Fixes #22120.
Fixes #22635.
Fixes #23776.
Fixes #29392.
Fixes #29916.
Fixes #31266.
Fixes #32625.
Fixes #33319.
Fixes #34669.
Fixes #35124.
Fixes #37339.
Fixes #38755.
Fixes #40202.
Fixes #43827.
Fixes #44218.
Fixes #46118.
Fixes #50580.
Fixes #52104.
Fixes #55528.
Fixes #56854.
Fixes #56973.