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