Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unions and intersections of type predicates produce wrong type #17757

Closed
SamPruden opened this issue Aug 12, 2017 · 9 comments
Closed

Unions and intersections of type predicates produce wrong type #17757

SamPruden opened this issue Aug 12, 2017 · 9 comments
Labels
Bug A bug in TypeScript Help Wanted You can do this
Milestone

Comments

@SamPruden
Copy link

SamPruden commented Aug 12, 2017

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.

@aluanhaddad
Copy link
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);

@SamPruden SamPruden changed the title Unions of type guard types produce wrong type Unions of type predicates types produce wrong type Aug 12, 2017
@SamPruden
Copy link
Author

type predicate

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

@SamPruden SamPruden changed the title Unions of type predicates types produce wrong type Unions and intersections of type predicates types produce wrong type Aug 12, 2017
@SamPruden SamPruden changed the title Unions and intersections of type predicates types produce wrong type Unions and intersections of type predicates produce wrong type Aug 12, 2017
@aluanhaddad
Copy link
Contributor

aluanhaddad commented Aug 13, 2017

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.

@SamPruden
Copy link
Author

SamPruden commented Aug 13, 2017

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.

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Aug 16, 2017
@RyanCavanaugh
Copy link
Member

Probably an easy fix if someone wants to try

@mhegazy mhegazy added the Help Wanted You can do this label Aug 23, 2017
@mhegazy mhegazy added this to the Community milestone Aug 23, 2017
@charlespierce
Copy link
Contributor

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

@ghost
Copy link

ghost commented Oct 6, 2017

@charlespierce By coincidence I made a commit just now fixing the union half in #17600. You could look at the intersection part once that's in; it's marked with // TODO: GH#17757.

@charlespierce
Copy link
Contributor

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

@jack-williams
Copy link
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.

@RyanCavanaugh RyanCavanaugh modified the milestones: Community, Backlog Mar 7, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Help Wanted You can do this
Projects
None yet
Development

No branches or pull requests

6 participants