Union Types in TypeScript

August 22, 20255 min read
Requirements:
FunctionsArraysObjects

When you first bump into Union Types in TypeScript, they can feel like a strange hybrid tool. They are not classes, not enums, not quite interfaces, yet they solve a very common problem in real world JavaScript.

Why union types matter

JavaScript is loose about data. An ID might be a string in one place and a number in another. An API could return either a result object or an error. Without a way to model this, you are left juggling any, runtime checks, or duplicated functions. Union Types let you say up front: this thing can be one of these specific types, nothing else. That gives you both safety and flexibility.

The basics: either or

A Union Type is written with a pipe character | between the options.

type UserID = number | string
 
let id: UserID
id = 42 // okay
id = 'alice' // also okay
// id = true;  // error

Here id is either a number or a string. TypeScript enforces that nothing outside those two slips in.

Narrowing down with type guards

Knowing something can be multiple types is only half the story. You often need to find out which one you actually have. This is called narrowing.

function formatInput(input: string | number) {
  if (typeof input === 'string') return input.toUpperCase()
  return input.toFixed(2)
}
interface Bird {
  layEggs(): void
  fly(): void
}
 
interface Fish {
  layEggs(): void
  swim(): void
}
 
type Pet = Bird | Fish
 
function handlePet(pet: Pet) {
  // Both Bird and Fish have layEggs, so this is safe
  pet.layEggs()
 
  // Only Fish has swim, so we need to check first
  if ('swim' in pet) {
    pet.swim() // TypeScript knows this is Fish
  } else {
    pet.fly() // TypeScript knows this is Bird
  }
}

Think of it like a Swiss Army knife. The handle could be many tools, but you check which blade is open before using it.

Literal unions for fixed options

Sometimes you want to lock values to a small set of strings.

type Direction = 'north' | 'south' | 'east' | 'west'
let turn: Direction
turn = 'north' // valid
turn = 'up' // error

This is a lightweight alternative to enums. Many developers prefer it because it adds no extra JavaScript output and works well with autocomplete.

This is also super useful for HTTP methods:

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'

Uniting object types

Union Types are not just for strings and numbers. They can describe complex structured data.

interface ErrorResult {
  success: false
  error: string
}
 
interface SuccessResult {
  success: true
  data: unknown
}
 
type Result = ErrorResult | SuccessResult

Here Result is either an error shape or a success shape. To safely use data, you first check result.success === true. That narrowing step avoids runtime surprises.

Discriminated unions: the tidy pattern

When you have several object shapes, add type field to each.

interface Circle {
  type: 'circle'
  radius: number
}
 
interface Square {
  type: 'square'
  sides: 4
}
 
type Shape = Circle | Square | Triangle

Then switch on shape.type. TypeScript will narrow correctly in each branch. If you later add a Triangle, the compiler will complain until you handle it.

interface Triangle {
  type: 'triangle'
  sides: 3
}

You can even add a helper like assertNever to guarantee every case is covered.

function area(shape: Shape) {
  switch (shape.type) {
    case 'circle':
      return Math.PI * shape.radius ** 2
    case 'square':
      return shape.sides ** 2
    // compiler will complain if you add a new shape without handling it
  }
}

Pitfalls to avoid

Union types versus enums and any

Enums give you runtime objects, unions are compile time only. Many developers prefer literal unions for their simplicity. Compared to any, unions are strict about what is allowed. They push you to handle all real possibilities, instead of leaving silent holes that turn into runtime errors.

A micro utility

If you often use discriminated unions, keep this helper:

function assertNever(x: never): never {
  throw new Error('Unexpected value: ' + x)
}

Drop it into a default case in a switch, and TypeScript will yell at you if you missed a new variant. It is a tiny safeguard that scales in large codebases.

Wrapping up

Union types are one of the most practical features in TypeScript. They let you capture the uncertainty of real world data without falling back on any. Learn to pair them with narrowing, literal unions, and discriminated unions. With a few small patterns like assertNever, you will catch mistakes at compile time instead of production.

Where could you apply a union in your own code this week? Maybe an API response, maybe a function that handles multiple input forms. Try one and see how much clearer your types become. And if you run into tricky cases, I would love to hear your stories.

Share this article

Stay Updated

Get the latest TypeScript tips, tutorials, and course updates delivered straight to your inbox.

No spam, unsubscribe at any time.