Skip to content

Destructured union of tuples with non-uniform length results in diagnostic when narrowing #49626

Open

Description

Bug Report

🔎 Search Terms

tuple type union destructuring narrowing control flow analysis non-uniform length varying length

🕗 Version & Regression Information

  • This appeared in TS 4.6 when the feature was introduced.

⏯ Playground Link

Playground link with relevant code

💻 Code

declare const [a, b, c]: ["a", number] | ["b", number, boolean];
if (a === "a") {
    // If this line is commented, the diagnostic is not thrown
    c;
} else {
    c;
}

🙁 Actual behavior

When c is accessed in the branch for ["a", number], it throws a diagnostic on the original declaration for c since the control flow analysis tries to access narrowedType[2] which does not exist on ["a", number]

🙂 Expected behavior

There should be no diagnostic thrown and c should simply be undefined (like it already is) or the diagnostic should be thrown where c is accessed rather than defined.

Activity

  1. RyanCavanaugh commented on Jun 22, 2022

    @RyanCavanaugh
    Member

    I don't see how either proposed behavior is necessarily better than the current one. It's very weird to get an error about "no element at position 2" on a bare identifier reference -- if you were many lines down in the function, that would seem like a bug, or possibly extremely misleading if you wrote something like arr[c].

  2. Fireboltofdeath commented on Jun 23, 2022

    @Fireboltofdeath
    Author

    I don't see how either proposed behavior is necessarily better than the current one. It's very weird to get an error about "no element at position 2" on a bare identifier reference -- if you were many lines down in the function, that would seem like a bug, or possibly extremely misleading if you wrote something like arr[c].

    That is a good point but I think the existing behavior is bad as it implies the destructuring is incorrect whereas indexing a possible member is valid everywhere else in TypeScript (including destructuring). I think not throwing a diagnostic would be more consistent and less confusing if someone does encounter this even if c's value isn't particularly useful.

  3. fatcerberus commented on Jun 23, 2022

    @fatcerberus

    Yeah, I get that none of the alternatives is necessarily ideal but having a code change somewhere produce an error in an earlier declaration that is possibly several screens away seems like the worst possible behavior. A valid declaration suddenly becoming invalid based on how later code uses it is something I’m pretty sure never happens any other time in TS.

  4. RyanCavanaugh commented on Jun 23, 2022

    @RyanCavanaugh
    Member

    If someone wants to put up a PR to make c correctly undefined that'd probably be worth evaluating

  5. callionica commented on Jun 23, 2022

    @callionica

    The situation shown with tuples is analogous to the situation with properties where you also get an error with this code:

    declare const { x, y, z }: { x:1, y: 1 } | { x:2, y:2, z:2 };
    

    The errors in both cases are correct. The third element or third property produces an error because the type as written does not really have a third element or property, it only has the potential of one.

    It sounds like you want to "collapse the wave function" and unify your provided type ["a", number] | ["b", number, boolean] to the conceptually similar, but actually distinct, type ["a" | "b", number, boolean | undefined].

    Unification is not an operation for which TS provides a helper, but you can implement it in a generic way so that you can unify any tuple. A working example is shown at this playground:

    Playground

  6. fatcerberus commented on Jun 23, 2022

    @fatcerberus

    Callionica (@callionica) Right, but the issue here is that c is only an error conditionally, if the code later tries to use it in the wrong branch. That’s really confusing, especially if the code that actually triggers the error isn’t on the same screen as the declaration where the error is being flagged.

  7. callionica commented on Jun 23, 2022

    @callionica

    Yes, that is different behaviour than the object destructuring example:
    declare const { x, y, z }: { x:1, y: 1 } | { x:2, y:2, z:2 };
    which triggers the compiler error pointing at z with no need to access z elsewhere.
    There should be similar behaviour in both cases.

  8. Fireboltofdeath commented on Jun 24, 2022

    @Fireboltofdeath
    Author

    Tuples can't have excess members. This is not the case with objects, so it can't reasonably conclude that z is undefined but in the tuple case it is safe to.

    playground

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions