I created a type guard on a generic class so I can safely use its generic member.
The way to determine the member's type is by looking at the parent's type
field.
In the code below, I use the type
field to decide what is the body type.
From outside the class, I can use the type guard and access the data member safely.
But when I use it inside an instance method on this
, I get compilation errors (see the comments in the code for the exact message in each case).
Why is this happening? Are there any restrictions on narrowing the type of the this
parameter?
type BodyType1 = {
field: string;
}
type BodyType2 = number[];
type BodyType = BodyType1 | BodyType2;
function isBodyType1(t: Message): t is Message<BodyType1> {
return t.type === 1;
}
function isBodyType2(t: Message): t is Message<BodyType1> {
return t.type === 2;
}
class Message<T extends BodyType = BodyType> {
type: number;
body: T;
constructor(type: number, body: T) {
this.type = type;
this.body = body;
}
getBodyField() {
const t = this;
if (isBodyType1(t)) {
// According to the compiler hint: const t: this & Message<BodyType1>
return t.body.field; // Compiler error: Property 'field' does not exist on type 'T'.ts(2339)
}
if (isBodyType2(t)) {
// According to the compiler hint: const t: this & Message<BodyType1>
return t.body[0]; // Compiler error: Property 'Body' does not exist on type 'never'.ts(2339)
}
}
}
const m = new Message(1, {field: 'value'})
if (isBodyType1(m)) {
// This compiles
console.log(m.body.field);
}
if (isBodyType2(m)) {
// This also compiles
console.log(m.body[0]);
}
CodePudding user response:
The only way I can see to sensibly do this is to have the type guards on the BodyType
itself:
function isBodyType1(t: BodyType): t is BodyType1 {
return (t as BodyType1).field != undefined;
}
function isBodyType2(t: BodyType): t is BodyType2 {
return (t as BodyType2).length != undefined
}
And then to use that:
class Message<T extends BodyType = BodyType> {
body: T;
constructor(body: T) {
this.body = body;
}
getBodyField() {
if (isBodyType1(this.body)) {
return this.body.field;
}
if (isBodyType2(this.body)) {
return this.body[0];
}
return null;
}
}
const m = new Message({field:"value"})
if (isBodyType1(m.body)) {
// This compiles
console.log(m.body.field);
}
if (isBodyType2(m.body)) {
// This also compiles
console.log(m.body[0]);
}
Having updated your question, and stating in comments that you need to use type
field in your type checks, the main problem is you're aliasing this
as t
which implicitly has an any
type. Don't do that!
You also need to introduce an interface with the type
property so that you can use it in your type guards. This works:
type BodyType1 = {
field: string;
}
type BodyType2 = number[];
type BodyType = BodyType1 | BodyType2;
interface IMessage{
type: number
}
function isBodyType1(t: IMessage): t is Message<BodyType1> {
return t.type === 1;
}
function isBodyType2(t: IMessage): t is Message<BodyType2> {
return t.type === 2;
}
class Message<T extends BodyType> {
type: number;
body: T;
constructor(type: number, body: T) {
this.type = type;
this.body = body;
}
getBodyField(): string | number | null {
if (isBodyType1(this)) {
return this.body.field;
}
if (isBodyType2(this)) {
return this.body[0];
}
return null;
}
}