If I have a base class and multiple sub classes that inherit from this base, e.g:
class Message {
constructor(public text: string) {}
}
class MessageA extends Message {
msgType: string = 'A';
constructor(
public text: string,
public aData: string
) {
super(text);
}
}
class MessageB extends Message {
msgType: string = 'B';
constructor(
public text: string,
public bData: number
) {
super(text);
}
}
… is it possible to create a type that only admits the sub classes?
I want something like
type Msg = MessageA | MessageB;
… that would allow me to write
const show = (msg: Msg): void => {
switch (msg.msgType) {
case 'A':
return console.log(`${msg.text}: ${msg.aData}`);
case 'B':
return console.log(`${msg.text}: ${msg.bData}`);
}
}
However, the code above gives me this error:
Property 'aData' does not exist on type 'Msg'.
Property 'aData' does not exist on type 'MessageB'.
If I change the type to be the following, show
works again:
type Msg = ({msgType: 'A'} & MessageA)
| ({msgType: 'B'} & MessageB);
But if I then call show
with a new MessageB
…
show(new MessageB('hello', 300));
… I get this error:
Argument of type 'MessageB' is not assignable to parameter of type 'Msg'.
Type 'MessageB' is not assignable to type '{ msgType: "B"; } & MessageB'.
Type 'MessageB' is not assignable to type '{ msgType: "B"; }'.
Types of property 'msgType' are incompatible.
Type 'string' is not assignable to type '"B"'.(2345)
What is a good way to create a type for the subclasses of a base class?
The goal here is to ensure that a handler will inspect the type of the object it was passed before it can access any fields (that aren't in the base class) and also ensure that the switch
is exhaustive for when future sub classes are added.
CodePudding user response:
It looks like you want Msg
to be a discriminated union, with msgType
as a discriminant property.
Unfortunately you have annotated the msgType
properties on MessageA
and MessageB
to be string
, and string
is not a valid discriminant property type. Discriminants must be unit types that only accept a single value, like undefined
or null
, or a string/numeric/boolean literal type.
Instead of string
, you want them to be of the string literal types "A"
and "B"
, respectively. You can do this either by explicitly annotating them as such, or by marking them as readonly
, which will cause the compiler to infer literal types for them:
class MessageA extends Message {
readonly msgType = 'A';
// (property) MessageA.msgType: "A"
constructor(
public text: string,
public aData: string
) {
super(text);
}
}
class MessageB extends Message {
readonly msgType = 'B';
// (property) MessageB.msgType: "B"
constructor(
public text: string,
public bData: number
) {
super(text);
}
}
Once you do this, the narrowing in your show()
function will just work:
const show = (msg: Msg): void => {
switch (msg.msgType) {
case 'A':
return console.log(`${msg.text}: ${msg.aData}`); // okay
case 'B':
return console.log(`${msg.text}: ${msg.bData}`); // okay
}
}