Reputation: 559
What is the exact reason for getting the error
Property 'descriminator' does not exist on type 'never'.
in line 67 and 70.
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
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