Description
๐ Search Terms
declare, assign, declaration, assignment, narrow, narrowing, union, variable, "control flow analysis"
โ Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
โญ Suggestion
Currently TypeScript performs assignment narrowing whenever a variable with a union type is written to.
I propose that assignment narrowing should not occur in the specific case where a variable is declared and initialized in a single statement with a type annotation (e.g. immediately following let a: B = c
, a
would be guaranteed to have exactly type B
). This behavior may need to live behind a compiler flag to avoid problems in existing code which assumes such narrowing happens. If the current narrowing behavior is desired at some particular assignment, it could be done in a separate statement (let a: B; a = c
).
There have been many issues filed related to assignment narrowing, but I haven't seen this specific idea suggested as a "feature request" anywhere. Apologies if I missed a duplicate.
๐ Motivating Example
There are tons of existing issues with relevant examples:
- why does initialization narrow type?ย #8513
- Control flow analysis fails to account for possible assignments to captured variables in closures.ย #30097
- let-union_type is narrowed incorrectlyย #31580
- Explicit boolean type not honored without castย #34691
- Type narrowing loses explicit type, forces me to re-cast to explicit typeย #36530
- Is type assignment a simple compatibility check? Or is it an assertion? TypeScript says "both, sometimes"ย #42656
- "Condition will always return false": declared type not being used?ย #42702
- Type of variable is narrowed immediately after definitionย #44824
- Type narrowing not working properly for unions in let declarationsย #48038
- TypeScript doesn't respect declared function argument type definitionย #51487
- Type inference ignores explicit type annotationย #55330
- Assingment type inference is broken with map, forEachย #61755
- โฆprobably many I missed
While some of the above touch on #9998 and related CFA limitations, as far as I can tell the specific examples in those issues would all cease being problematic if this suggestion were implemented.
I think type inference has trained me (and perhaps others) to take a mental shortcut: it's easy to conceptualize most assignments as if the type checker examined the right-hand side first (before dealing with the left), in which case it feels like the : B
should "win" in let a: B = c
and act as an upcast (because : B
would be applied after determining the type of c
). I know that's not literally how things work, but the actual behavior is harder to explain and keep front-of-mind at all times. This is made worse by the fact that when B
is a non-union type then it does act like an upcast (e.g. a
is unknown
rather than string
after let a: unknown = 'blah'
).
๐ป Use Cases
-
What do you want to use this for?
Declaring a union-typed variable and giving it an initial value without having to work around the type checker. -
What shortcomings exist with current approaches?
As far as I can tell the suggested approach is usually a type assertion (instead oflet a: B = c
, writelet a = c as B
). Shortcomings of that approach include:- it's not always obvious when it will be necessary, especially when the union is hiding behind a type alias
- it's easy to accidentally assert the wrong type (or change the type/value in a future refactor) without getting any feedback, leading to runtime bugs that could have easily been caught by the type checker
- if you have a lint rule banning type assertions it needs to be disabled
-
What workarounds are you using in the meantime?
Type assertions as mentioned above, or an identity function.The "safe upcast" version of
satisfies
would have provided another workaround, but that's not the version that shipped.