Type Guards and Narrowing in Typescript

31 May 2022

Type guards (also known as Narrowing) allow us create conditions under which an object of one type can be used as if it is of another type. We usually use this in conjunction with union types to allow us to specify different handling of the types based on the resulting value

Using typeof

We can use the typeof keyword in javascript to find out whether what type an object is. This is useful if we have an object that can take on different structures, for example

type Data = string[]
type GetData = Data | (() => Data)

In the above example, we have a type called GetData which can be either some data or a function to get data. Using this, we can can create a function which fetches data like so:

const fetchData = (getData: GetData): Data => {
  if (typeof getData === 'function') {
    return getData()
  }

  return getData
}

Using in

Javascript also has the in operator which can be used to infer types by us checking a property of an object

type SimpleData = { 
  name:string;
}

type ComplexData = {
  name: {
    first: string;
    last: string;
  }
  isComplex: true;  
}

type AnyData = SimpleData | ComplexData

We can then use the in operator to check the existence of a property of an object by using it along with a key that we expect to be in one object but not another

const getComplexName = (data: AnyData): string => {
  // isComplex is the name of the key that we expect in `ComplexData` but not `SimpleData`
  if ('isComplex' in data) {
    return [data.name.first, data.name.last].join(' ')
  }

  return data.name
}

Using is

We can use the typescript is keyword to specify that the return of a boolean means that a variable satisfies a specific condition

For example, we can create a function that basically does what the in operator in the above function does:

const isComplex = (data: AnyData): data is ComplexData => {
  return (data as ComplexData).isComplex
}

This can be used in place of the in check in the above example like so:

const getComplexName2 = (data: AnyData): string => {
  // isComplex is the name of the key that we expect in `ComplexData` but not `SimpleData`
  if (isComplex(data)) {
    return [data.name.first, data.name.last].join(' ')
  }

  return data.name
}

References