Skip to content

Incorrect narrowing when a type guard returns a type union #41871

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

Closed
octogonz opened this issue Dec 8, 2020 · 2 comments · Fixed by #49625
Closed

Incorrect narrowing when a type guard returns a type union #41871

octogonz opened this issue Dec 8, 2020 · 2 comments · Fixed by #49625
Labels
Duplicate An existing issue was already created

Comments

@octogonz
Copy link

octogonz commented Dec 8, 2020

TypeScript Version: 4.1.2

Search Terms:

narrowing narrowed type guard type union

Code

@MickeyPhoenix encountered this bug while implementing type guards for HBOMax data services. The original repro involves class inheritance, like this:

playground link

class Animal {
    private x: number = 0;
}
class Cat extends Animal {
    public whiskers: number = 1;
}
class Broom {
    private y: number = 1;

    public whiskers: number = 1;
    public handle: number=3;
}

function hasWhiskers(input: Animal | Broom): input is Cat | Broom {
    return input instanceof Cat || input instanceof Broom;
}

function incorrect(input: Animal | Broom): number {
    if (hasWhiskers(input)) {
        // INCORRECT: There should be a compiler error here, 
        // but `input` got incorrectly narrowed to `Broom` 
        // (instead of `Cat | Broom`)
        return input.handle; 
    } 
    return -1;
}

function correct(input: Animal | Broom): number {
    if (input instanceof Cat || input instanceof Broom) {
        // CORRECT: `handle` is not a member of `Cat | Broom`,
        // so the compiler correctly reports an error here.
        return input.handle;
    } 

    return -1;
}

const cat = new Cat();
// Prints "undefined" because cat.handle didn't actually exist
console.log(incorrect(cat));

But it can also be reproduced using simpler interface inheritance, like this:

playground link

interface Animal {
    legs: number;
}
interface Cat extends Animal {
    whiskers: number;
}
interface Broom {
    whiskers: number;
    handle: number;
}

function hasWhiskers(input: Animal | Broom): input is Cat | Broom {
    return Object.hasOwnProperty.call(input, 'whiskers');
}

function incorrect(input: Animal | Broom): number {
    if (hasWhiskers(input)) {
        // INCORRECT: There should be a compiler error here, 
        // but `input` got incorrectly narrowed to `Broom` 
        // (instead of `Cat | Broom`)
        return input.handle; 
    } 
    return -1;
}

Expected behavior:

One would expect identical type narrowing when extracting this:

function f(input: Animal | Broom): number {
    if (input instanceof Cat || input instanceof Broom) {
    . . .

...into this:

function hasWhiskers(input: Animal | Broom): input is Cat | Broom {
    return input instanceof Cat || input instanceof Broom;
}

. . .

function f(input: Animal | Broom): number {
    if (hasWhiskers(input)) {
    . . .

Actual behavior:

The type is incorrectly narrowed to Broom instead of Cat | Broom.

As a result, clearly incorrect code compiles without any error.

Related Issues: #31156

@andrewbranch
Copy link
Member

This is indeed a duplicate of #31156.

@andrewbranch andrewbranch added the Duplicate An existing issue was already created label Dec 8, 2020
@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' 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
Labels
Duplicate An existing issue was already created
Projects
None yet
3 participants