Home > Software engineering >  Using a type guard on this
Using a type guard on this

Time:02-16

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]);
}

Playground link


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

Playground link

  • Related