Type Guards

About

A type guard is TS technique to get information about a type of a variable. I think that type checking cannot be done cleanly without type guards.

TypeScript has some built-in JS operators like typeof, instanceof, and the in operator. Type guards allows the TS compiler to infer a specific type for a variable.

Type guards are typically used for narrowing down a type.

There are five major ways to use a type guard:

  1. The instanceof keyword

  2. The typeof keyword

  3. The in keyword

  4. Equality narrowing type guard

  5. Custom type guard with predicate

The instanceof Type Guard

instanceof are used to check if a value is an instance of a given constructor function or class.

class SomeClass {
  foo = 123
  common = 'hello world'
}

class AnotherClass {
  bar = 456
  common = 'hello world'
}

function doSth(x: SomeClass | AnotherClass) {
  if (x instanceof SomeType) {
    console.log(x.foo)
    console.log(x.bar) // Error
  }
  if (x instanceof AnotherClass) {
    console.log(x.foo) // Error
    console.log(x.bar)
  }
  
  console.log(x.common)
  console.log(x.foo) // Error
  console.log(x.bar) // Error
}
  • TS is also aware of the context within if and else, so that we can narrow down types.

The typeof Type Guard

The typeof type guard is used to determine the type of a variable, but is said to be very limited. It can only determine the following types recognized by JS:

  • boolean

  • string

  • bigint

  • symbol

  • undefined

  • function

  • number

For anything not on the list, the typeof will return object.

The syntax is like this:

  • typeof v === 'typename'

  • typeof v !== 'typename'

where typename can be a string, number, symbol, or boolean.

function doSth(x: number | string) {
  if (typeof x === 'string') {
    console.log(x.doSthCrazy(1)) // Error: doSthCrazy is not defined in 'string'
    console.log(x.substr(1))
  }
  x.substr(1) // Error: x can also be a number
}

The in Type Guard

The in type guard checks if an object has a particular property, using it to differentiate between different types.

The syntax is like this:

  • propertyName in objectName

interface SomeType {
  x: number
}

interface AnotherType {
  y: string
}

function doSth(sth: SomeType | AnotherType) {
  if ('x' in sth) {
    // sth: SomeType
  } else {
    // sth: AnotherType
  }
}

Equality Narrowing Type Guard

Equality narrowing checks for the value of an expression.

For two variables to be equal, both variables must be of the same type. If the type of a variable is not known, but it's equal to another variable with precise type, then TS will narrow the type of the first variable with the information the second variable provides.

function getValues(a: number | string, b: string) {
  if (a === b) {
    // This is where the narrowing takes place. `a` is narrowed to string.
    console.log(typeof a) // string
  } else {
    // If there's no narrowing, type just remains unknown
    console.log(typeof a) // number | string
  }
}

Custom Type Guard with Predicate

This method is what I find the most interesting among the five major type guard techniques.

It's basically creating a function to check whether a variable belongs to a type and return a boolean result.

This method is the most powerful of all, but is also error-prone if you're not careful with how you define the function.

interface Necklace {
  kind: string
  brand: string
}

interface Bracelet {
  brand: string
  year: number
}

type Accessory = Necklace | Bracelet

const isNecklace = (a: Accessory): a is Necklace => {
  return (a as Necklace).kind !== undefined
}

const necklace: Accessory = { kind: 'Choker', brand: 'TAKASI' }
const bracelet: Accessory = { brand: 'Cartier', year: 2021 }

console.log(isNecklace(necklace)) // true
console.log(isNecklace(bracelet)) // false

if (isNecklace(necklace)) {
  // Do something with necklace as Necklace ...
}

Enum Type Checks with Type Guards

Custom type guards are especially useful when checking types for a variable in enum array.

export type UserType = 'developer' | 'designer'

interface UserView {
  id: string
  name: string
  type: UserType
}

const USER_TYPES = ['developer', 'designer']

function isDeveloper(userType: string): userType is UserType {
  return USER_TYPES.includes(userType)
}

Be careful not to use in keyword instead of includes. Those two are totally different from each other!

REF

Last updated