Operator IS in Typescript

Viewed 1186

Can anyone explain how exactly the operator is works in typescript for type guards? There are some interfaces and custom type guards below.

  interface kekType {
   kek: string;
  }
  interface kekType2 {
   kek: string;
   lol: string;
  }
  interface kekType3 {
   keks: string;
  }

  const funckek = (akek: kekType, istrue: boolean): akek is kekType2 => istrue;
  const funckek2 = (akek: object, istrue: boolean): akek is kekType2 => istrue;

And two variables which will be casting.

  let aaa = {} as kekType;
  let aaa2 = {} as kekType3

Is it create type for if/else scope.

  if (funckek(aaa, true)) {
   console.log(aaa); // let aaa: kekType2
  }
  
  console.log(aaa); // let aaa: kekType

  if (funckek2({} as kekType3, true)) {
   console.log(aaa2); // let aaa2: kekType3
  }  

Or is it somehow combine those types if their types cannot be changed.

  if (funckek2(aaa2, true)) {
   console.log(aaa2); // let aaa2: kekType3 & kekType2
  }

I really do not understand how it actually works. I hope someone can shed some light on this.

2 Answers

Think of a typeguard in TypeScript as a function that does two things. First, it returns a boolean. In this sense,

const isString(x:unknown): x is string = typeof x === 'string'

and

const isString(x:unknown): boolean = typeof x === 'string'

are equivalent. However, the typeguard provides one additional thing beyond what the function-returning-boolean provides: it helps TypeScript narrow possible types in your code.

Suppose you have the following code:

const isString = (x:unknown):x is string => typeof x === 'string'
const isBoolean = (x:unknown):x is boolean => typeof x === 'boolean'
const multiplyByTwo = (x:number) => x*2

function myFn(a: string|number|boolean) {
  // right here, typescript knows 'a' could be a string, number, or boolean
  multiplyByTwo(a) // Error! 'multiplyByTwo' cannot take a string or boolean

  if(isString(a)) return
  // now here, typescript knows 'a' could only be a number or boolean
  multiplyByTwo(a) // Error! 'multiplyByTwo' cannot take a boolean

  if(isBoolean(a)) return
  // now here, typescript knows 'a' could only be a number
  multiplyByTwo(a) // Success
}

I do not know what the istrue parameter in your typeguards is meant to do, but if I were writing typeguards for your domain, I would write them as

const isKekType = (a: unknown): a is kekType => 
  typeof a === 'object' && a.hasOwnProperty('kek')
const isKekType2 = (a: unknown): a is kekType2 =>
  typeof a === 'object' && a.hasOwnProperty('kek') && a.hasOwnProperty('lol')
const isKekType3 = (a: unknown): a is kekType3 =>
  typeof a === 'object' && a.hasOwnProperty('keks')

Please note that TypeScript is "duck-typed," which means that even though your kekType2 does not explicitly extend kekType, it has all the properties that kekType does, so any kekType2 will also be a kekType.

You could make your typeguards even more specific if you really wanted to and ignore the duck typing like so:

const isKekType = (a: unknown): a is kekType => 
  typeof a === 'object' && a.hasOwnProperty('kek') && Object.keys(a).length === 1
const isKekType2 = (a: unknown): a is kekType2 =>
  typeof a === 'object' && a.hasOwnProperty('kek') && a.hasOwnProperty('lol') && Object.keys(a).length === 2
const isKekType3 = (a: unknown): a is kekType3 =>
  typeof a === 'object' && a.hasOwnProperty('keks') && Object.keys(a).length === 1

Best practice is that your typeguard function takes only one parameter: the value you're checking the type of. So I wouldn't pass an istrue: boolean second parameter in typeguards.

For Classes:

You can add a readonly property of the name "type" to every class and check this property to get the type of the class.

class A {
   public readonly type = "A"
   public kek = "someValue"
}
class B {
   public readonly type = "B"
   public kek = "randomValue"
   public lol = "LoL"
}
class C {
   public readonly type = "C"
   public keks = "keks"
}

You can do now the following in a function:

function (someObject: A | B | C) {
  switch(someObject.type) {
          case "A":
            // do something (someObject is automatically casted to class A)
            break
          case "B":
            // do something (someObject is automatically casted to class B)
            break
          case "C":
            // do something (someObject is automatically casted to class C)
            break
          default: 
            throw new Error("someObject has an invalid type")
        }
}
Related