Skip to content

Conditional type does not narrow union type #29188

Description

TypeScript Version: 3.2.2

Search Terms: conditional types, unions, narrowing

Code

    interface A<T> {
        value: T;
    }

    interface Specification {
        [key: string]: Array<any> | Specification;
    }

    type Mapping<S extends Specification> = {
        [key in keyof S]: S[key] extends Array<infer T> ? A<T> : Mapping<S[key]>
        // Error                                                         ^^^^^^
        // Type 'S[key]' does not satisfy the constraint 'Specification'.
        //   Type 'Specification[key]' is not assignable to type 'Specification'.
        //     Type 'any[] | Specification' is not assignable to type 'Specification'.
        //       Type 'any[]' is not assignable to type 'Specification'.
        //         Index signature is missing in type 'any[]'.
    };

Expected behavior:
No error. "Leafs" of the Specification tree, which have type Array<T> (for some T) should be mapped to A<T>, while non-leaf properties should be recursively mapped.

Actual behavior:
In the right-hand side of the conditional type, S[key] is not narrowed to Specification, even if the complete type of S[key] is Array<any> | Specification and the Array<any> case is catched in the left-hand side.

Playground Link: link

Related Issues: some similar issues related to conditional types, but I'm not sure whether this is a duplicate of any of them.

Activity

  1. fatcerberus commented on Dec 28, 2018

    @fatcerberus

    Not 100% positive but I think this might have to do with the way conditional types interact with unions. The conditional type distributes over a union, i.e. it acts like a map operation. In other words the ternary is evaluated individually for each type in the union rather than treating it as a unit, and the final type is a union of that.

  2. lodo1995 commented on Dec 28, 2018

    @lodo1995
    Author

    Apparently it can be fixed by reversing the order and adding a second conditional to perform the inference on the right-hand side

    type Mapping<S extends Specification> = {
        [key in keyof S]: S[key] extends Specification ? Mapping<S[key]>
                        : S[key] extends Array<infer T> ? A<T> : never
    };
    

    To be honest, this simple case doesn't even need a conditional, because you can get the element type of the array by doing A<S[key][0]>. But my real use case involves more complex types, including inference of function signatures, which can only be achieved with infer, as far as I know.

  3. jack-williams commented on Jan 3, 2019

    @jack-williams
    Collaborator

    Conditional types do not produce substitution types for the false branch of the conditional (a.k.a do not narrow in the false branch).

    There was an attempted fix here: #24821, however this was closed.

  4. lmcarreiro commented on Jan 3, 2019

    @lmcarreiro

    Not sure if this is the same, but the compiler are behaving differently if I have an union of arrays vs array of union:

    type A = { x: string };
    type B = { x: string, y: number };
    
    const arr1: (A | B)[] = [];
    arr1.find(e => e.x === ""); // OK
    arr1.map(e => e.x); // OK
    
    const arr2: A[] | B[] = [];
    arr2.find(e => e.x === ""); // OK
    arr2.map(e => e.x); // Error: Cannot invoke an expression whose type lacks a call signature.

    Shouldn't this arr2.map(e => e.x) work?

  5. jack-williams commented on Jan 3, 2019

    @jack-williams
    Collaborator

    Leonardo Carreiro (@lmcarreiro)

    This is a very different situation, and it is normal that a union of arrays behaves differently to an array of unions in some cases. There probably is some justification that the map should work, however this case involves synthesising a union of call signatures which is pretty complex. I would recommend reading over this PR by Wesley Wigham (@weswigham), #29011, and any related issues. If you still have some queries I think it would be better to start a new issue, or reply to those, as this topic is very different to the initial question in this thread.

  6. weswigham commented on Jan 3, 2019

    @weswigham
    Member

    Jack Williams (@jack-williams) had identified the core of the issue. We use "substitution" types internally to track the constraints applied to a type within the true branch of a conditional, however we do no such tracking for the false branch. This means that you can't actually bisect a union type with a conditional right now, as Lodovico Giaretta (@lodo1995) points out, you must chain two conditions and invert the check so your logic is in the true branch instead.

    Part of the reason why we didn't move forward with #29011 (other than one of the relations I identified not holding up under scrutiny) is that tracking falsified constraints with substitution types kinda works... but when you perform the substitution, the information is lost, since we do not currently have a concept of a "negated" type (I mitigated this a little bit by remateriaizling the substitutions that tracked negative constraints late, but that's a bit of a hack). We cannot say that the given T extends string ? "ok" : T that the type of T in the false branch is a T & ~string , for example - we do not have the appropriate type constructors currently.

    We regularly bring up how we really do probably need it for completeness, but the complexity "negated" types bring is... large? At least that's what we seem to think - it's not immediately obvious that a ~string is an alias for "any type except those which are or extend string", and therefore that a ~string & ~number is "any type except strings or numbers" (note how despite the use of &, the english you read as used the word "or").

    So we're very aware of what needs to be done to make this work better... we're just having trouble convincing ourselves that it's "worth it".

  7. fatcerberus commented on Jan 3, 2019

    @fatcerberus

    therefore that a ~string & ~number is "any type except strings or numbers" (note how despite the use of &, the english you read as used the word "or").

    Personally I first read this as “all values which are not string AND not number”—only later switching to a negated “or” to simplify the clause mentally.

    Maybe that’s just because I’ve been coding too long, though... 😛 I quickly recognize !(x || y) to be the same as (!x && !y) and vice versa.

  8. falsandtru commented on Feb 2, 2019

    @falsandtru
    Contributor

    Duplicate of #21937.

  9. TotallyNotChase commented on Jan 7, 2021

    @TotallyNotChase

    Alongside the narrowing mentioned here, Is it possible to add narrowing of union object types using in operator for conditional types?

    E.g-

    type Foo = string | { type: string };
    
    type Bar<T extends string> = T;
    
    type Qux<T extends Foo> = {
        [K in keyof T]: T[K] extends string ? Bar<T[K]> : T[K] extends { type: string } ? Bar<T[K]['type']> : never;
    };

    It'd be nicer if this was possible instead-

    type Qux<T extends Foo> = {
        [K in keyof T]: 'type' in T[K] ? Bar<T[K]['type']> : Bar<T[K]>;
    };
  10. RyanCavanaugh commented on Aug 27, 2026

    @RyanCavanaugh
    Member

    S[key] is not narrowed to Specification in the false branch. It remains a type involving the generic S; treating that branch as “S[key] excluding Array<any>” would require representing a negated type.

    This is the behavior described in Why can't I narrow or assign to a generic type parameter the way I can with its constraint?: generic conditional types are not narrowed by outer control flow in the same way as concrete unions. TypeScript does not currently provide the required negated-type operation; see Exclude Isn't Type Negation and #4196.

    The nested conditional in the follow-up comment works because the recursive Mapping<S[key]> reference is moved into the true branch, where the Specification constraint can be tracked.

  11. added
    Needs Human ReviewThis issue has a backlog check awaiting maintainer review.
    Design LimitationConstraints of the existing architecture prevent this from being fixed
    and removed
    BugA bug in TypeScript
    Needs Human ReviewThis issue has a backlog check awaiting maintainer review.
    on Aug 27, 2026
  12. weswigham commented on Aug 27, 2026

    @weswigham
    Member

    Also, just... ref #63926.

  13. typescript-automation commented on Aug 30, 2026

    @typescript-automation
    Contributor

    This issue has been marked as "Design Limitation" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

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

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions