TypeScript: Using typecasting for typeguard

51 Views Asked by At

I have class with constructor that can accept object of two types, and then based on what type of object it got, perform different actions.

I've seen that it's possible to use as keyword for that. But it feels wrong to me.

export type Angle = {
    cos: number; 
    sin: number
}
export type VectorAngleInit = {
    angle: Angle,
    magnitude: number
}
export type VectorDInit = {
    dx: number,
    dy: number
}
export class Vector {
    #dx: number;
    #dy: number;
    constructor(init: VectorAngleInit | VectorDInit) {
        if ((init as VectorAngleInit)?.angle) {
            const angleInit = init as VectorAngleInit
            this.#dx = angleInit.magnitude * angleInit.angle.cos;
            this.#dy = angleInit.magnitude * angleInit.angle.sin;
        } else {
            const dInit = init as VectorDInit
            this.#dx = dInit.dx
            this.#dy = dInit.dy
        }
    }
}

Should i do that another way? If so, which way is better you think?

2

There are 2 best solutions below

0
jcalz On BEST ANSWER

You can use in operator narrowing to check for the presence of "angle" in init and narrow init to the desired union member without a need for type assertions (what you're calling "casting"):

export class Vector {
  #dx: number;
  #dy: number;
  constructor(init: VectorAngleInit | VectorDInit) {
    if ("angle" in init) {
      this.#dx = init.magnitude * init.angle.cos;
      this.#dy = init.magnitude * init.angle.sin;
    } else {
      this.#dx = init.dx
      this.#dy = init.dy
    }
  }
}

Playground link to code

0
ColaFanta On

The thing you need is called Type Predicate which is an official way to narrow down your union type. It is a function with a special return type x is SomeType which is actually a boolean but informs typescript compiler that what you actually needs to do is to predicate the type rather than just return a boolean.

A sample related to your problem could be this:

First define your type predicate function

export type VectorAngleInit = {
  angle: Angle
  magnitude: number
}

// type predicate predicating whether u is of type VectorAngleInit
export function isVectorAngleInit(u: unknown): u is VectorAngleInit {
  return !!u && typeof u === 'object' && (u as VectorAngleInit).angle instanceof Angle
}

export type VectorDInit = {
  dx: number
  dy: number
}

// type predicate predicating whether u is of type VectorDInit
export function isVectorDInit(u: unknown): u is VectorDInit {
  return !!u && typeof u === 'object' && typeof (u as VectorDInit).dx === 'number'
}

And then use it in your constructor:

export class Vector {
  #dx: number
  #dy: number
  constructor(init: VectorAngleInit | VectorDInit) {
    if (isVectorAngleInit(init)) {
      // notice that type of `init` is auto inferred within if block
      this.#dx = init.magnitude * init.angle.cos
      this.#dy = init.magnitude * init.angle.sin  
    }
    if(isVectorDInit(init)){
      this.#dx = init.dx
      this.#dy = init.dy  
    }
  }
}