skip to content

Nate Lentz

An introduction to Typescript Discriminated Unions

/ 2 min read

Learn a Typescript pattern for reducing unnecessary optional values and improving type safety.

Discriminated Unions

Typescript Unions provide us a powerful feature that allows discrimination between different Union members.

Discriminated Unions offer an elegant solution for handling all possible object variations reducing the risk for runtime errors. Typescript uses Discriminated Unions to infer types based on a property called the Discriminator Property.

The below example does not use a Discriminated Union. You can see how your code may have to perform some additional safety checks to determine the shape of the data. Additionally, we must mark majority of our properties as optional due to the multiple possible states for an ApiResponse to be in.

interface ApiResponse<T = any> {
  status: 'success' | 'error'
  code: number
  data?: T
  error?: {
    message: string
  }
}

In this instance, Typescript will not be able to provide proper type safety. Typescript would allow you to access properties which may not be present at runtime.

const handleApiResponse = (response: ApiResponse) {
  switch(response.status) {
    case 'success':
      // When status is success, error shouldn't exist.
      console.log(response.error?.message)
      break
    case 'error':
      // When status is error, data shouldn't exists
      console.log(response.data)
      break
    default:
      break
  }
}

Let’s fix this with a Discriminated Union. We have a Union type called ApiResponse composed of the Union between ApiSuccess and ApiError. In this case, each member has a literal type which we can use as the discriminator.

interface ApiSuccess<TData = any> {
  status: 'success',
  code: number
  data?: TData
}

interface ApiError {
  status: 'error',
  code: number
  error: {
    message: string
  }
}

type ApiResponse = ApiSuccess | ApiError

const handleApiResponse = (response: ApiResponse) => {
  switch(response.status) {
    case 'success':
      // ERROR: error does not exist on type ApiSuccess
      console.log(response.error)
      break
    case 'error':
      // ERROR: data does not exist on type ApiError
      console.log(response.data)
      break
    default:
      break
  }
}

In the handleApiResponse function, the switch expression is the discriminated property which allows Typescript to infer the union member type for each case. When this happens, Typescript is smart enough to tell us when we try to access properties which do not exist on the member. This improves our developer experience and code readability. Pretty neat.

TypeScript discriminated unions provide flexibility for working with object types and object variations. The improved type safety makes discriminated unions a must have in your developer toolkit.

Read more about everyday types with Typescript.