Skip to content

Suggestion: Type narrowing also narrows conditional types #21879

Closed
@krryan

Description

@krryan

TypeScript Version: 2.8.0-dev.20180204

Code

declare function broke(impossible: never): never; // used to ensure full case coverage

interface Foo { kind: 'foo'; }
declare function isFoo(foobar: Foo | Bar): foobar is Foo;
interface Bar { kind: 'bar'; }
declare function isBar(foobar: Foo | Bar): foobar is Bar;

function mapFooOrBar<FooBar extends Foo | Bar, R>(
    foobar: FooBar,
    mapFoo: FooBar extends Foo ? ((value: Foo) => R) : ((impossible: never) => never),
    mapBar: FooBar extends Bar ? ((value: Bar) => R) : ((impossible: never) => never),
): R {
    if (isFoo(foobar)) {
        return mapFoo(foobar);
/*             ^^^^^^^^^^^^^^
Cannot invoke an expression whose type lacks a call signature. Type '((value: Foo) => R) | ((impossible: never) => never)' has no compatible call signatures. */
    }
    else if (isBar(foobar)) {
        return mapBar(foobar);
/*             ^^^^^^^^^^^^^^
Cannot invoke an expression whose type lacks a call signature. Type '((value: Bar) => R) | ((impossible: never) => never)' has no compatible call signatures. */
    }
    else {
        return broke(foobar as never);
    }
}

(the casting foobar as never for the broke function is a workaround for #20375)

Expected Behavior
Compile without errors.

Actual Behavior
The types of mapFoo and mapBar are not reconsidered in light of passing the typeguard:
Cannot invoke an expression whose type lacks a call signature. Type '((value: Foo) => R) | ((impossible: never) => never)' has no compatible call signatures.

Cannot invoke an expression whose type lacks a call signature. Type '((value: Bar) => R) | ((impossible: never) => never)' has no compatible call signatures.

My use case for this is a situation where I have a union of three types, but some cases will only ever use a union of two of those types. I want this kind of utility function to be re-usable for those situations, but indicate that the handlers for unincluded cases will never be called (and allow us to type-safely stub those branches, e.g. with that broke function).

Our project uses these kinds of unions and utility functions a lot and this would significantly improve them.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions