Skip to content

Type narrowing not working for unions of tuples with object literals #31613

Closed

Description

It's not possible to narrow unions of tuples with object literals

TypeScript Version: 3.4.5, 3.5.0-rc

Search Terms: tuple narrow object literals

Code:

Narrowing tuples with primitive types works as expected:

type X = ["a", "c"] | ["b", "b"]

function bar(x: X) {
  if (x[0] === "b") {
    return x[1] === "c" // correctly narrows to "c", shows "condition always false"
  }
}

However, when the tuple contains object literals, narrowing doesn't work at all:

interface A { data: "a" }
interface B { data: "b" }
interface C { data: "c" }

type Y = [A, C] | [B, B]

function foo(y: Y) {
  if (y[0].data === "b") {
    return y[1].data === "c" // incorrectly narrows to "c" | "b"
  }
}

Expected behavior:

The object literal within the tuple is correctly narrowed down to type B

Actual behavior:

The object literal is not narrowed, leaving it at type C | B.

Playground Link: Link

Related Issues:
#26323
#24120
#10530

Activity

  1. changed the title [-]Type narrowing for unions of tuple elements with object literals[/-] [+]Type narrowing not working for unions of tuples with object literals[/+] on May 28, 2019
  2. midzdotdev commented on Aug 1, 2019

    @midzdotdev

    Hi guys, not sure if this helps but here's another example which I believe to be the same issue:

    type FieldArray = string[]
    type FieldSet = Record<string, any>
    
    type FieldParams =
      | [ FieldArray, FieldSet ]
      | [ FieldArray ]
      | [ FieldSet ]
    
    function convertFieldParams (fields: FieldParams): Field[] {
      const normalised = typeof fields[1] === 'undefined'
        ? fields[0] instanceof Array
          ? [ fields[0], {} ]
          : [ [], fields[0] ]
        : fields
    
      // ...
    }

    I would expect the normalised variable to have a type of [ FieldArray, FieldSet ] due to the type checking, although it's showing in my editor as Record<string, any>[].

    I found a workaround by doing this:

    const normalised: [ FieldArray, FieldSet ] = 
        fields.length === 1
          ? fields[0] instanceof Array
            ? [ fields[0], {} ]
            : [ [], fields[0] ]
          : fields
  3. squidfunk commented on Aug 9, 2019

    @squidfunk
    Author

    Any update? Fixing this issue would make tuples much more powerful :-)

  4. GeeWee commented on Sep 3, 2019

    @GeeWee

    Probably same as #12849
    I've narrowed the issue down a little bit, as to where it breaks down:
    playground link

    type DataFetchingResponse2 = [number, 'loaded'] | [null, 'error' | 'loading'];
    
    
    function test2(): DataFetchingResponse2 {
    	return [null, 'error'];
    }
    
    function testTypeNarrowing() {
    	const res2 = test2();
    
        // This works fine and correctly narrows res2[0] down to number
    	if (res2[1] == 'loaded') {
    		const data2 = res2[0];
    	}
    
        
        // This does not, but casts it as a number|null
        const status = res2[1]
    	if (status == 'loaded') {
    		const data2 = res2[0];
    	}
    
        // This also does not, as it thinks dataFromDestructuring is number|null inside the type narrowing
        const [dataFromDestructuring, statusFromDestructuring] = res2
    	if (statusFromDestructuring == 'loaded') {
    		console.log(dataFromDestructuring)
    	}
    }
    

    So narrowing directly on the tuple type works fine, but when destructuring or assigning to an intermediate variable, it does not narrow correctly.

  5. squidfunk commented on Sep 3, 2019

    @squidfunk
    Author

    Gustav Wengel (@GeeWee): I think that those are effectively two separate bugs, as the issue describes type narrowing without destructuring on tuples with primitive (working) and non-primitive (not working) values.

  6. GeeWee commented on Sep 3, 2019

    @GeeWee

    I think you're probably right actually.

  7. DanielRosenwasser commented on Sep 3, 2019

    @DanielRosenwasser
    Member

    Looks like this is probably a bug in isMatchingReference.

  8. Kingwl commented on Sep 13, 2019

    @Kingwl
    Contributor

    I'd like to try this if no one started work

  9. sandersn commented on Sep 13, 2019

    @sandersn
    Member

    Sounds good, I wasn't planning to start on it until after the beta starts.

  10. 4 remaining items

  11. sandersn commented on Jan 10, 2020

    @sandersn
    Member

    This is actually not specific to tuples. Control-flow narrowing doesn't work with any nested properties:

    interface A { data: "a" }
    interface B { data: "b" }
    interface C { data: "c" }
    
    type Y = { a:A, b:C } | { a:B, b:B }
    
    function foo(y: Y) {
      if (y.a.data === "b") {
        return y.b.data === "c"
      }
    }

    It probably could be done but would require us to either (1) re-think handling of unions of object types or (2) how to relate entity-named discriminants that share a prefix.

  12. jtbandes commented on Jun 21, 2022

    @jtbandes
    Contributor

    Possibly related: #48378

  13. squidfunk commented on Jun 22, 2022

    @squidfunk
    Author

    3 years old, still not fixed. Is this issue dead? It was first scheduled for 3.7 and then repeatedly rescheduled until it ended in the backlog. Fixing this would help in several use cases and greatly improve expressivity. I have several parts in my code base where I need to fall back to very generic types that are not helpful at all what could be a union of tuples.

  14. rauschma commented on May 3, 2025

    @rauschma

    The following doesn’t work either:

    function f(...args: [number] | [string]) {
      if (typeof args[0] === 'string') {
        args; // [number] | [string]
      }
    }

    Useful when using tuples to help with overloading:

    function re(flags: string): (templateStrings: TemplateStringsArray, ...substitutions: unknown[]) => RegExp;
    function re(templateStrings: TemplateStringsArray, ...substitutions: unknown[]): RegExp;
    function re(...args:
      | [flags: string]
      | [templateStrings: TemplateStringsArray, ...substitutions: unknown[]]
    ): any {
      // ...
    }
  15. RyanCavanaugh commented on Sep 2, 2026

    @RyanCavanaugh
    Member

    This is a duplicate of #18758. Both reports request that checking a nested discriminant narrow the containing union; tuple positions versus named outer properties do not change that underlying control-flow narrowing limitation.

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

    BugA bug in TypeScriptDomain: check: Control FlowThe issue relates to control flow analysisNeeds ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions