Typescript: Unions and intersections of type predicates produce wrong type

Created on 12 Aug 2017  ·  9Comments  ·  Source: microsoft/TypeScript

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.

Bug help wanted

Most helpful comment

type predicate

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

All 9 comments

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);

type predicate

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

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.

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.

Probably an easy fix if someone wants to try

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

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

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

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

blendsdk picture blendsdk  ·  3Comments

wmaurer picture wmaurer  ·  3Comments

MartynasZilinskas picture MartynasZilinskas  ·  3Comments

fwanicka picture fwanicka  ·  3Comments

kyasbal-1994 picture kyasbal-1994  ·  3Comments