Skip to content

Skip assignment narrowing when declaring & initializing a variable in the same statementย #61789

Open
@mkantor

Description

@mkantor

๐Ÿ” Search Terms

declare, assign, declaration, assignment, narrow, narrowing, union, variable, "control flow analysis"

โœ… Viability Checklist

โญ 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:

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

  1. 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.

  2. What shortcomings exist with current approaches?
    As far as I can tell the suggested approach is usually a type assertion (instead of let a: B = c, write let 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
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DeclinedThe issue was declined as something which matches the TypeScript visionSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions