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.
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 areaPattern 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.
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] LeafSingle-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
1. What happens if you add a new case to a discriminated union that is already matched against in several places in your codebase?
2. Given type Shape = | Circle of radius: float | Rectangle of width: float * height: float, how do you construct a Rectangle with width 3.0 and height 4.0?
3. Why are recursive discriminated unions well suited to representing a binary tree?
4. What is the main benefit of a single-case DU like type CustomerId = CustomerId of string over just using a plain string?
Was this page helpful?
You May Also Like
Option Types in F#
Understand F#'s option type as a safe, explicit alternative to null, and how to combine option values with map, bind, and defaultValue.
Generic Types in F#
Explore how F# generic types and automatic type inference let you write reusable code that works across many types safely.
Structural Equality
Learn why F#'s default equality compares records, tuples, and unions by value instead of by reference, and when to opt out.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics