Skip to content

F# Guide — Discriminated Unions: Modeling Choices and Variants

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about F# Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

Discriminated unions (DUs) in F# define types that can represent one of several possible variants, each potentially carrying different types of data, making them ideal for modeling domain choices and states.

What You'll Learn

  • Defining discriminated unions
  • Pattern matching on union cases
  • Single-case DUs for type safety
  • Recursive DUs for trees
  • Option and Result as built-in DUs

Why It Matters

DUs capture domain logic in the type system, making illegal states unrepresentable. The compiler ensures all cases are handled in match expressions. Durga Antivirus Pro uses DUs for scan results and threat classifications.

Real-World Use

DUs model payment methods, order states, network responses, tree structures, and any domain with distinct variants.

flowchart LR
    A["Discriminated Unions"] --> B["Definition"]
    B --> C["Pattern Matching"]
    C --> D["Single-Case DUs"]
    D --> E["Recursive DUs"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Defining DUs

// Simple enum-like DU
type Color =
    | Red
    | Green
    | Blue

// DU with data
type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float
    | Triangle of base: float * height: float

// DU with named fields
type Employee =
    | Manager of name: string * department: string
    | Developer of name: string * language: string
    | Intern of name: string

Pattern Matching on DUs

type Shape =
    | Circle of float
    | Rectangle of float * float
    | Triangle of float * 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

area (Circle 5.0)  // 78.54
area (Rectangle (4.0, 6.0))  // 24.0

Option Type

// Option is a built-in DU
// type Option<'T> = Some of 'T | None

let divide x y =
    if y = 0 then None else Some (x / y)

let describe opt =
    match opt with
    | Some value -> sprintf "Result: %d" value
    | None -> "Division by zero"

// Optional fields in records
type Person = {
    Name: string
    MiddleName: string option
}

Result Type

// Result is a built-in DU
// type Result<'T, 'Error> = Ok of 'T | Error of 'Error

type CalculationError = DivisionByZero | NegativeInput

let safeSqrt x =
    if x < 0 then Error NegativeInput
    else Ok (sqrt x)

let safeDivide x y =
    if y = 0 then Error DivisionByZero
    else Ok (x / y)

Single-Case DUs

// Wrapping primitive types for type safety
type Email = Email of string
type PhoneNumber = PhoneNumber of string

let createEmail (s: string) =
    if s.Contains("@") then Some (Email s)
    else None

// Prevents mixing up emails and phone numbers
// Both are strings but the type system distinguishes them

type CustomerId = CustomerId of int
type OrderId = OrderId of int

Recursive DUs

// Tree structure
type Tree<'T> =
    | Leaf of 'T
    | Node of Tree<'T> * Tree<'T>

// Expression tree
type Expr =
    | Number of int
    | Add of Expr * Expr
    | Multiply of Expr * Expr
    | Variable of string

let rec evaluate env expr =
    match expr with
    | Number n -> n
    | Add (a, b) -> evaluate env a + evaluate env b
    | Multiply (a, b) -> evaluate env a * evaluate env b
    | Variable name -> Map.find name env

Common Mistakes

1. Not covering all cases

The compiler warns about non-exhaustive matches. Add missing cases or a wildcard.

2. Overusing flat DUs

Deeply nested DUs with many cases are hard to maintain. Keep DUs focused.

3. Forgetting data in cases

If a case needs data, add it: Case of data. If not, just use Case.

4. Confusing DU cases with values

Cases are constructors, not values. Red is a constructor, not a string or int.

5. Publicly exposing internal DU representation

Use DU internally but expose through functions with controlled construction.

Practice Questions

1. What is a discriminated union? A type that can be one of several named cases, each potentially carrying different associated data.

2. Why use single-case DUs? To wrap primitive types for type safety, preventing mix-ups between values with the same underlying type.

3. How does Option differ from null? Option is a DU with explicit Some and None cases. The compiler enforces handling both cases. Null is a language loophole.

Challenge: Model a traffic light system with a DU and write a function that returns the next light state.

FAQ

{{< faq question="Are DUs like enums in C#?" >}} DUs are more powerful. Like enums, but each case can carry different types and amounts of data. {{< /faq >}}

{{< faq question="Can DUs be recursive?" >}} Yes. DUs can reference themselves in their definitions, enabling tree and list structures. {{< /faq >}}

{{< faq question="How many cases can a DU have?" >}} As many as needed. But very large DUs (10+ cases) may indicate a design issue. Consider splitting. {{< /faq >}}

{{< faq question="Can DUs implement interfaces?" >}} Yes. DUs can implement interfaces, useful for interoperability with C# code. {{< /faq >}}

{{< faq question="Are DUs reference or value types?" >}} DUs are reference types by default. Add [<Struct>] for value-type semantics. {{< /faq >}}

Mini Project

Model a calculator with discriminated unions:

type Op = Add | Subtract | Multiply | Divide
type Expr =
    | Literal of float
    | Binary of Op * Expr * Expr

let rec eval expr =
    match expr with
    | Literal n -> n
    | Binary (Add, a, b) -> eval a + eval b
    | Binary (Subtract, a, b) -> eval a - eval b
    | Binary (Multiply, a, b) -> eval a * eval b
    | Binary (Divide, a, b) -> eval a / eval b

// (3 + 4) * 2
let expr = Binary (Multiply, Binary (Add, Literal 3.0, Literal 4.0), Literal 2.0)
eval expr  // 14.0

What's Next

Now that you understand discriminated unions, explore the Option type in depth.

Topic Description Link
F# Option Types Using Option in depth {{< ref "12-option-types" >}}
F# Result Types Result type for error handling {{< ref "13-result-types" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro