Search code examples
typescripttypeguardsnarrowing

How can I use instanceof inside a custom type guard in typescript?


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
}

Solution

  • 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
    }
    

    Playground Link


    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 of x contains Foo.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
    }
    

    Playground Link