Gleam Custom Types

Custom types let you define your own data shapes. They describe every possible form a value can take — including forms that carry different data depending on which variant they are. This is one of Gleam's most powerful features and the foundation of safe, expressive code.

What Is a Custom Type?

A custom type defines a set of named constructors. Each constructor represents one possible form of the type. Think of it like a labelled box factory — you define the shapes of boxes, and every value is one of those shapes.

type Direction {
  North
  South
  East
  West
}

Custom Type Diagram
──────────────────────────────────────────────────
type Direction has 4 variants:

 North ─┐
 South ─┤── all are of type Direction
 East  ─┤
 West  ─┘

Using a Custom Type

let heading = North
let destination = East

let label = case heading {
  North -> "Going up"
  South -> "Going down"
  East  -> "Going right"
  West  -> "Going left"
}

The compiler verifies that case covers all variants. Add a new variant to the type and every case that misses it produces a compile error — so you never forget to update the logic.

Variants With Data

Constructors can carry data. Different variants can carry different shapes of data:

type Shape {
  Circle(radius: Float)
  Rectangle(width: Float, height: Float)
  Triangle(base: Float, height: Float)
}

Shape Variants
──────────────────────────────────────────────────
Circle(5.0)            → one field: radius
Rectangle(10.0, 4.0)   → two fields: width, height
Triangle(6.0, 3.0)     → two fields: base, height

All three are of type Shape.
pub fn area(shape: Shape) -> Float {
  case shape {
    Circle(r)       -> 3.14159 *. r *. r
    Rectangle(w, h) -> w *. h
    Triangle(b, h)  -> b *. h /. 2.0
  }
}

Mixing Variants With and Without Data

type Coin {
  Heads
  Tails
}

type PaymentStatus {
  Pending
  Paid(amount: Float)
  Refunded(amount: Float, reason: String)
  Failed(error_code: Int)
}

Pending carries no data — it is just a label. Paid, Refunded, and Failed each carry relevant information. Pattern matching extracts that information:

pub fn describe_payment(status: PaymentStatus) -> String {
  case status {
    Pending             -> "Waiting for payment"
    Paid(amount)        -> "Paid: ₹" <> float.to_string(amount)
    Refunded(amt, why)  -> "Refunded ₹" <> float.to_string(amt) <> " — " <> why
    Failed(code)        -> "Failed with code " <> int.to_string(code)
  }
}

Recursive Custom Types

A custom type can refer to itself. This creates tree-shaped or list-shaped data structures:

type Tree {
  Leaf
  Node(value: Int, left: Tree, right: Tree)
}

pub fn sum_tree(tree: Tree) -> Int {
  case tree {
    Leaf              -> 0
    Node(v, left, right) -> v + sum_tree(left) + sum_tree(right)
  }
}

Binary Tree Diagram
──────────────────────────────────────────────────
       Node(10)
      /        \
  Node(5)    Node(15)
  /    \      /    \
Leaf  Leaf  Leaf  Leaf

sum_tree = 10 + 5 + 15 = 30

Generic Custom Types

Custom types accept type parameters to work with any value type:

type Box(a) {
  Empty
  Full(value: a)
}

let int_box: Box(Int)    = Full(42)
let str_box: Box(String) = Full("hello")
let empty:   Box(Int)    = Empty

This is exactly how Gleam's built-in Option and Result types are defined — as generic custom types.

Practical Example — Traffic Light System

import gleam/io

type TrafficLight {
  Red
  Amber
  Green
}

pub fn next_state(light: TrafficLight) -> TrafficLight {
  case light {
    Red   -> Green
    Green -> Amber
    Amber -> Red
  }
}

pub fn instruction(light: TrafficLight) -> String {
  case light {
    Red   -> "Stop"
    Amber -> "Prepare to stop"
    Green -> "Go"
  }
}

pub fn main() {
  let light = Red
  io.println(instruction(light))           // Stop
  let next = next_state(light)
  io.println(instruction(next))            // Go
  let after = next_state(next)
  io.println(instruction(after))           // Prepare to stop
}

Traffic Light Cycle
──────────────────────────────────────────────────
Red → Green → Amber → Red → ...

Key Points


Custom Types Essentials
──────────────────────────────────────────────────
1. Define with: type Name { Variant1, Variant2, ... }
2. Variants can carry zero or more named fields
3. Pattern match to extract variant and its data
4. Compiler enforces exhaustive case handling
5. Types can be recursive (refer to themselves)
6. Types can be generic: type Box(a) { ... }
7. Every value is exactly one variant at runtime

Custom types eliminate entire categories of bugs. Instead of using magic strings like "pending" or magic numbers like 1 to represent states, you create explicit named variants that the compiler understands and verifies throughout your codebase.

Leave a Comment

Your email address will not be published. Required fields are marked *