Daniel C.
Daniel C.

Reputation: 559

Type guard doesn't work on sub classes with same interface

What is the exact reason for getting the error

Property 'descriminator' does not exist on type 'never'.

in line 67 and 70.

See stackblitz here

type DescriminatorType = 'SubClassA' | 'SubClassB' | 'SubClassC'

Define some abstract base class with static type guard functions

export abstract class BaseClass {

  constructor(
    public descriminator: string,
  ) {
  }

  // tpye-guard function for SubClassA
  public static isSubClassA(o: BaseClass): o is SubClassA {
    return o instanceof SubClassA;

    // Or
    // return o.descriminator === 'SubClassA';
  }

  // tpye-guard function for SubClassB
  public static isSubClassB(o: BaseClass): o is SubClassB {
    return o instanceof SubClassB;

    // Or
    // return o.descriminator === 'SubClassA';
  }

  // tpye-guard function for SubClassC
  public static isSubClassC(o: BaseClass): o is SubClassC {
    return o instanceof SubClassC;

    // Or
    // return o.descriminator === 'SubClassA';
  }
}

Define some sub classes. Currently two of them have the same interface.

export class SubClassA extends BaseClass {
  constructor(
  ) {
    super('SubClassA');
  }
}

export class SubClassB extends BaseClass {
  constructor(
  ) {
    super('SubClassB');
  }
}

export class SubClassC extends BaseClass {

  constructor(
    public subClassProp1: number,
  ) {
    super('SubClassC');
  }
}

Define some program logic which make usage of type-guard functions:

export class OtherClass {
  data = [new SubClassA(), new SubClassA(), new SubClassB(), new SubClassC(123)]
  public doSomething(): string[] {

    return this.data.map(d => {
      if (BaseClass.isSubClassA(d)) {
        return d.descriminator;
      }
      else if (BaseClass.isSubClassB(d)) {
        return d.descriminator;   // Error: Property 'descriminator' does not exist on type 'never'.
      }
      else if (BaseClass.isSubClassC(d)) {
        return d.descriminator;   // Error: Property 'descriminator' does not exist on type 'never'.
      }
      else {
        return 'UNKNOWN TYPE';
      }
    })
  }
}

As you can see in the StackBlitz on runtime everything works fine. I'm getting the expected output!

Upvotes: 2

Views: 673

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250016

The typescript type system is structural in nature. If two types are declared separately such as BaseClass and SubClassA but have the same structure, they are the exact same type as far as the type system is concerned (at least in most practical cases)

Type guards work on the else branch, by taking out of the union the type that was matched by the type guard. Since SubClassB has the same structure as SubClassA it is also taken out of the union (essentially they are the same type under a different name)

SubClassC is taken out because the type guard will also take out subtypes of the matched type, and since SubClasssA is the same type as BaseClass, SubClassC is technically speaking, a subtype of SubClassA

All this leads to the else branch of the if having no types left to handle, and d will be of type never

The simplest solution in this case is to keep the discriminator as a type parameter on BaseClass this way the classes will be structurally different (with this type parameter being used as the type of discriminator, unused type parameters don't count towards structure):

type DescriminatorType = 'SubClassA' | 'SubClassB' | 'SubClassC'

export abstract class BaseClass<T extends DescriminatorType = DescriminatorType> {

    constructor(
        public descriminator: T,
    ) {
    }

    // tpye-guard function for SubClassA
    public static isSubClassA(o: BaseClass): o is SubClassA {
        return o instanceof SubClassA;

        // Or
        // return o.descriminator === 'SubClassA';
    }

    // tpye-guard function for SubClassB
    public static isSubClassB(o: BaseClass): o is SubClassB {
        return o instanceof SubClassB;

        // Or
        // return o.descriminator === 'SubClassA';
    }

    // tpye-guard function for SubClassC
    public static isSubClassC(o: BaseClass): o is SubClassC {
        return o instanceof SubClassC;

        // Or
        // return o.descriminator === 'SubClassA';
    }
}

export class SubClassA extends BaseClass<'SubClassA'> {
    constructor(
    ) {
        super('SubClassA');
    }
}

export class SubClassB extends BaseClass<'SubClassB'> {
    constructor(
    ) {
        super('SubClassB');
    }
}

export class SubClassC extends BaseClass<'SubClassC'> {

    constructor(
        public subClassProp1: number,
    ) {
        super('SubClassC');
    }
}

export class OtherClass {
    data = [new SubClassA(), new SubClassA(), new SubClassB(), new SubClassC(123)]
    public doSomething(): string[] {

        return this.data.map(d => {
            if (BaseClass.isSubClassA(d)) {
                return d.descriminator;
            }
            else if (BaseClass.isSubClassB(d)) {
                return d.descriminator;
            }
            else if (BaseClass.isSubClassC(d)) {
                return d.descriminator;
            }
            else {
                return 'UNKNOWN TYPE';
            }
        })
    }
}

Upvotes: 3

Related Questions