Discriminated Unions

typescript programming

A brief tutorial on Typescript discriminated unions

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”

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:

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:

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.

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);
    }
}