Skip to content

Unions and intersections of type predicates produce wrong type #17757

Description

@SamPruden

TypeScript Version: 2.5.0-dev.20170803

Code

type Foo = typeof ts.isDoStatement | typeof ts.isWhileStatement;

Expected behavior:
Resulting type should be (node: ts.Node) => node is (ts.DoStatement | ts.WhileStatement)

Actual behavior:
Resulting type is (node: ts.Node) => node is ts.DoStatement

It seems to just take the first one.

Activity

  1. aluanhaddad commented on Aug 12, 2017

    @aluanhaddad
    Contributor

    It appears to just grab the first type predicate and ignore the rest.

    Here is a self-contained repro (2.5.0-dev.20170808)

    type IsStringOrNumber = ((x: any) => x is number) | ((x: any) => x is string);
    
    declare const x: any;
    
    if ((((x: any) => true) as IsStringOrNumber)(x)) {
        x.toFixed();
    }

    Intersecting type predicates seems to have the same result, only the first signature is considered.

    type IsStringAndNumber = ((x: any) => x is number) & ((x: any) => x is string);
  2. changed the title [-]Unions of type guard types produce wrong type[/-] [+]Unions of type predicates types produce wrong type[/+] on Aug 12, 2017
  3. SamPruden commented on Aug 12, 2017

    @SamPruden
    Author

    type predicate

    I knew using "type guards" like that wasn't quite right. 😂

  4. changed the title [-]Unions of type predicates types produce wrong type[/-] [+]Unions and intersections of type predicates types produce wrong type[/+] on Aug 12, 2017
  5. changed the title [-]Unions and intersections of type predicates types produce wrong type[/-] [+]Unions and intersections of type predicates produce wrong type[/+] on Aug 12, 2017
  6. aluanhaddad commented on Aug 13, 2017

    @aluanhaddad
    Contributor

    Being able to compose these functions would be highly useful.

    Imagine a rather heterogeneous array elements of elements in a scenario such as

    import moment from 'moment';
    
    interface Partial {
      name?: string;
      id?: number;
      dob?: moment.Moment
    }
    
    declare const partials: Partial[];
    
    type HasName = (x: Partial) => x is {name: string};
    type HasDob = (x: Partial) => x is {dob: moment.Moment};
    type HasNameAndDob = HasName & HasDob;
    
    const hasNameAndDob: HasNameAndDob = ({name, dob}) => name && dob;
    
    const withNamesAndDobs = mayHaveProps.filter(hasNameAndDob);

    There is some boilerplate in the example, but throw in a compose helper and it would make for some very nice patterns.

  7. SamPruden commented on Aug 13, 2017

    @SamPruden
    Author

    I was thinking of this more as a bug report than a feature request, but if we're doing demonstrations of value, this was my scenario.

    The typescript compiler has lots of Node types differentiated by a kind enum property, but no discriminated union type exists to allow nicely switching on kinds. The compiler itself does lots of ugly and dangerous asserting to get around this. There are, however, ts.isWhileStatement(node: Node) style functions for almost all nodes.

    I was trying to put together something like this:

    function isAnyOf<T extends ts.Node>(node: ts.Node, ...preds: Array<(n: ts.Node) => n is T>): node is T {
        return preds.some(p => p(node));
    }
    
    if (isAnyOf(node, ts.isWhileStatement, ts.isIfStatement)) {
        // node should have type ts.WhileStatement | ts.IfStatement
        // actually just has type ts.WhileStatement
    }

    But with the current behaviour/bug, T just takes the type of whatever the first predicate is and completely ignores the others.

  8. RyanCavanaugh commented on Aug 16, 2017

    @RyanCavanaugh
    Member

    Probably an easy fix if someone wants to try

  9. added this to the milestone on Aug 23, 2017
  10. charlespierce commented on Oct 6, 2017

    @charlespierce
    Contributor

    Ryan Cavanaugh (@RyanCavanaugh) I'm interested in tackling this issue, can you point me towards what changes will be needed? I was able to find (and fix) an issue where Type Predicates weren't being correctly handled by getContextualSignature in checker.ts, however that didn't seem to change the behavior at all. It seems that getContextualSignature isn't actually used inside of resolveCall so the Type Predicate still isn't being correctly determined.

  11. charlespierce commented on Oct 6, 2017

    @charlespierce
    Contributor

    Andy (Andrewkraft) (@Andy-MS) Thanks, I'll take a look at the intersection part. I actually had just figured out what I was missing, but I'm glad to see I came up with essentially the same solution as you for the union signatures.

  12. jack-williams commented on Feb 10, 2019

    @jack-williams
    Collaborator

    I think this issue can be closed. The union case correctly works by selecting both predicate types:

    type IsStringOrNumber = ((x: any) => x is number) | ((x: any) => x is string);
    
    declare const x: any;
    
    if ((((x: any) => true) as IsStringOrNumber)(x)) {
        x.toFixed(); // x has type number | string
    }

    The intersection case still selects the first overload, but this is a general problem with intersections of signatures---it is not a particular issue with type predicates.

  13. locked as resolved and limited conversation to collaborators on Oct 21, 2025
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 TypeScriptHelp WantedYou can do this

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions