I'm trying to create a custom type guard using an instanceof
but strangely it isn't working as expected in the else clause
This is an example with the related playground link: Playground Link
class Person {}
class Animal {}
const isPerson = (obj: Person | Animal): obj is Person => obj instanceof Person;
const isAnimal = (obj: Person | Animal): obj is Animal => obj instanceof Animal;
const test: Person | Animal = new Person();
if(test instanceof Animal){
test; // const test: Animal
}
else {
test; // const test: Person
}
if(isAnimal(test)){
test; // const test: Animal
}
else {
test; // const test: never
}
I would expect to have test to be of type Person
in the else clause, but it is of type never
... why?
I know that I can also use directly the instanceof, but I would prefer to have a more concise function like those created
UPDATE: With this little edit the type guard is working right... why?! Playground Link
class Person {
private xxx = "xxx"
}
class Animal {
private xxx = "xxx"
}
const isPerson = (obj: Person | Animal): obj is Person => obj instanceof Person;
const isAnimal = (obj: Person | Animal): obj is Animal => obj instanceof Animal;
declare const test: Person | Animal;
if(test instanceof Animal){
test; // const test: Animal
}
else {
test; // const test: Person
}
if(isAnimal(test)){
test; // const test: Animal
}
else {
test; // const test: Person
}
TypeScript performs structural typing. And it sees that Person
and Animal
actually have the same structure.
So if test is Animal
is false
, it infers that test is Person
is false
as well. Hence the resulting never
type left.
By having unique properties in Person
and Animal
, now their structure is different, and TypeScript correctly gives you the Person
type left in the else
block.
class Person {
common = 0
//person = 0
}
class Animal {
common = 0
animal = 0
}
const isPerson = (obj: Person | Animal): obj is Person => obj instanceof Person;
const isAnimal = (obj: Person | Animal): obj is Animal => obj instanceof Animal;
declare const test: Person | Animal;
if (test instanceof Animal) {
test; // const test: Animal
}
else {
test; // const test: Person
}
if (isAnimal(test)) {
test; // const test: Animal
}
else {
test; // const test: Person
}
Note: beware of const test: UnionType = oneOfTheTypes;
, TypeScript performs assignment narrowing, so it it is still able to infer that test
is actually one of the types only.
On the other hand, narrowing with instanceof
works differently:
in JavaScript
x instanceof Foo
checks whether the prototype chain ofx
containsFoo.prototype
.
TypeScript obviously replicates this behaviour, hence it does not apply structural typing, but prototype inheritance check.
After question edit:
By adding a unique field (or a private field) to Animal
, we explicitly break the possibility that Person
somehow extends Animal
. It is a similar situation to my above code sample.
So now TypeScript treats them as 2 totally unrelated types, and the control flow behaves as you expect (i.e. test
is a Person
in the else
block).
On the contrary, should Person
extend Animal
, we now explicitly relate the 2 types, and TypeScript now knows that if test
is not an Animal
, it is not a Person
either (hence test
is back to never
in the else
block), including when using instanceof
! (Since in that case we create a class inheritance)
class Animal {
private xxx = "xxx"
}
class Person extends Animal {
private xxx2 = "xxx"
}
const isPerson = (obj: Person | Animal): obj is Person => obj instanceof Person;
const isAnimal = (obj: Person | Animal): obj is Animal => obj instanceof Animal;
declare const test: Person | Animal;
if (test instanceof Animal) {
test; // const test: Animal | Person
}
else {
test; // const test: never
}
if (isAnimal(test)) {
test; // const test: Animal | Person
}
else {
test; // const test: never
}