Closed
Description
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:
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:
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