---
title: Discriminated Unions
description: A brief tutorial on Typescript discriminated unions
date: 2025-07-25
tags: [typescript, programming]
url: https://jdeiss.com/posts/discriminated-unions
---

import YoutubeEmbed from "../../components/YoutubeEmbed.astro"


A "Discriminated Union" is a useful pattern in structural languages.

First, let's understand what a union is. *This* is a union-- although not very useful. You can think of it as meaning "string *or* number"

```ts
type NewType = string | number

let a: NewType = "hello"  // ok
let b: NewType = 5 // ok
let c: NewType = {} // throws an error! {} is not a string or number!
```


Unions are more useful with objects; say an `Action` type that can be one of many different types:
```ts
interface AttackAction {
    target: string;
    damage: number;
    weapon: string;
}

interface HealAction {
    patient: string;
    heal: number;
}

interface MoveAction {
    to: { x: number, y: number };
    speed: number;
}

type Action = 
    | HealAction
    | MoveAction
    | AttackAction
```

The problem comes in when we are given a plain `Action` type--since types are only a compile time feature, we have no way to tell what specific *kind* of action we are given at runtime 

This is where the "discriminated" part comes in--we just directly indicate the type of the action as a unique string literal property.

We put a special string literal `type` or `kind` property in each interface:
```ts
interface AttackAction {
    type: "attack"
    target: string;
    damage: number;
    weapon: string;
}

interface HealAction {
    type: "heal";
    patient: string;
    heal: number;
}

interface MoveAction {
    type: "move";
    to: { x: number, y: number };
    speed: number;
}

type Action = 
    | HealAction
    | MoveAction
    | AttackAction
```

This means we can now write simple conditionals and let the typescript compiler automatically figure out what type of action we have to achieve both simple and typesafe code.

```ts
function performAction(action: Action) {
    switch (action.type) {
        case "attack":
            // compiler now knows that `action` is `AttackAction` if we are in this branch
            console.log(attack.damage);
            break;
        case "move":
            // we are now a move action
            console.log(attack.to);
    }
}

```

<YoutubeEmbed embedPath="m_PUMru1CqI" />
