Skip to content

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

Closed
@octogonz

Description

@octogonz

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions