100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Discriminated Unions

Learn how F#'s discriminated unions model a closed set of named cases, each optionally carrying data, with compiler-enforced exhaustive pattern matching.

Type SystemIntermediate9 min readJul 10, 2026
Analogies

What Is a Discriminated Union?

A discriminated union (DU) in F# is a type built from a fixed set of named cases, each of which can carry its own data or no data at all, declared with syntax like type PaymentMethod = | Cash | Card of number: string * expiry: string | Wallet of provider: string. Every value of type PaymentMethod is tagged internally with which case constructed it, so the compiler always knows, at compile time, the complete set of shapes a value can take -- nothing outside those declared cases is possible, which is what people mean when they say F# makes illegal states unrepresentable.

🏏

Cricket analogy: Think of how a delivery in cricket is officially scored as exactly one outcome -- dot ball, single, boundary, wide, no-ball, or wicket -- never an ambiguous mix; a DU like type BallOutcome = Dot | Runs of int | Wicket of Dismissal forces every ball to be tagged as precisely one of those, just like the scorer's ledger.

Defining and Constructing DU Cases

DU cases can carry no data (like a simple enum case), a single value, or multiple values as a tuple, and F# lets you name those fields for clarity, as in type Shape = | Circle of radius: float | Rectangle of width: float * height: float | Triangle of baseLen: float * height: float. Constructing a value is just calling the case name as a function -- Circle 5.0 or Rectangle (3.0, 4.0) -- and the compiler infers the resulting type is Shape regardless of which case was used, so a list like [Circle 5.0; Rectangle (3.0, 4.0)] type-checks even though the two elements were built by different constructors.

🏏

Cricket analogy: Naming DU fields is like a bowling analysis line that doesn't just say '3 wickets' but labels overs, maidens, runs, and wickets separately -- Bowling of overs: float * maidens: int * runs: int * wickets: int -- so anyone reading Bowling(10.0, 2, 34, 3) knows exactly which number means what, the way Ravichandran Ashwin's figures are always presented with labeled columns.

fsharp
type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float
    | Triangle of baseLen: float * height: float

let area shape =
    match shape with
    | Circle r -> System.Math.PI * r * r
    | Rectangle (w, h) -> w * h
    | Triangle (b, h) -> 0.5 * b * h

let shapes = [ Circle 5.0; Rectangle (3.0, 4.0); Triangle (6.0, 2.0) ]
let totalArea = shapes |> List.sumBy area

Pattern Matching on Discriminated Unions

Consuming a DU is done with match, which requires (or at least strongly warns about) covering every case: match shape with | Circle r -> Math.PI * r * r | Rectangle (w, h) -> w * h | Triangle (b, h) -> 0.5 * b * h. The F# compiler performs exhaustiveness checking, so if a new case like Square of side: float is added to the Shape type later, every match expression across the codebase that doesn't handle Square produces an FS0025 'incomplete match' warning, turning what would be a silent runtime bug in many other languages into a compile-time signal pointing at every call site that needs updating.

🏏

Cricket analogy: Exhaustiveness checking is like the third umpire's protocol requiring a decision for every referred ball -- out, not out, or umpire's call -- leaving no delivery unresolved; if the ICC added a new category like 'soft signal overturned', every review procedure document referencing the old three outcomes would need updating, just as FS0025 flags every unmatched call site.

If you add a new case to a DU that's matched in dozens of places across a codebase, F# only warns (FS0025) by default rather than erroring -- set <WarningsAsErrors>FS0025</WarningsAsErrors> in your .fsproj, or add a catch-all | _ -> failwith "unhandled case" sparingly, to make sure exhaustiveness gaps actually block a build rather than silently shipping.

Recursive Discriminated Unions and Trees

Because a DU case can reference the type being defined, DUs naturally express recursive structures like trees and linked lists: type Tree<'a> = | Leaf | Node of Tree<'a> * 'a * Tree<'a> defines a binary tree where a Node holds a left subtree, a value, and a right subtree, and a Leaf marks an empty branch. Functions over recursive DUs are typically written as recursive functions that pattern-match on the structure, such as an insert function that recurses into the left or right subtree depending on a comparison, or a depth function that returns 0 for Leaf and 1 + max (depth l) (depth r) for Node(l, _, r).

🏏

Cricket analogy: A recursive DU like a binary tree mirrors a knockout tournament bracket -- the Cricket World Cup semi-final slot either holds 'Leaf' (unplayed, TBD) or a 'Node' pointing to the two teams that must first win their quarter-finals, and computing the bracket's depth recurses down exactly like depth walks a Tree.

fsharp
type Tree<'a> =
    | Leaf
    | Node of Tree<'a> * 'a * Tree<'a>

let rec insert value tree =
    match tree with
    | Leaf -> Node (Leaf, value, Leaf)
    | Node (l, v, r) ->
        if value < v then Node (insert value l, v, r)
        elif value > v then Node (l, v, insert value r)
        else tree

let rec depth tree =
    match tree with
    | Leaf -> 0
    | Node (l, _, r) -> 1 + max (depth l) (depth r)

let sample = List.foldBack insert [5; 3; 8; 1; 4] Leaf

Single-case discriminated unions like type CustomerId = CustomerId of string are a common F# idiom for wrapping a primitive so the compiler stops you from accidentally passing a raw string where a CustomerId was expected -- this is sometimes called the 'wrapper type' or 'newtype' pattern.

  • A discriminated union groups a fixed set of named cases under one type, each optionally carrying its own data.
  • Values are constructed by calling a case name like a function, e.g. Circle 5.0.
  • Named fields (Circle of radius: float) improve readability over positional tuples.
  • Match expressions are the primary way to consume a DU, and the compiler performs exhaustiveness checking.
  • Adding a new DU case produces FS0025 warnings at every match expression that doesn't yet handle it.
  • DU cases can reference their own type, enabling recursive structures like trees and linked lists.
  • Single-case DUs are a common pattern for wrapping primitives to add type safety.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#FStudyNotes#DiscriminatedUnions#Discriminated#Unions#Union#Defining#StudyNotes#SkillVeris#ExamPrep