Gleam Records
Records group related values together under named fields. Where tuples use position to identify values, records use names. Named fields make code self-documenting — you always know what each piece of data means without counting positions.
Defining a Record
Records in Gleam are defined as custom types with a single constructor:
type Person {
Person(name: String, age: Int, email: String)
}
Record Type Anatomy
──────────────────────────────────────────────────
type Person {
Person(name: String, age: Int, email: String)
│ │ │ │
│ └── constructor name │
└── type name └── field with type
}
The type name (Person) and the constructor name (Person) are the same here — this is the standard convention for single-variant types.
Creating a Record
let user = Person(
name: "Sunita",
age: 31,
email: "sunita@example.com"
)
You use the field labels when creating a record. The labels make the creation call clear — no guessing which value goes in which position.
Accessing Record Fields
Use dot notation to read a field:
let name = user.name // "Sunita"
let age = user.age // 31
let email = user.email // "sunita@example.com"
Field Access Diagram
──────────────────────────────────────────────────
user = Person("Sunita", 31, "sunita@example.com")
user.name → "Sunita"
user.age → 31
user.email → "sunita@example.com"
Updating a Record
Records are immutable. To "change" a field, create a new record with the updated value using the spread syntax:
let older_user = Person(..user, age: 32)
// Name and email stay the same, age becomes 32
Record Update Syntax
──────────────────────────────────────────────────
Original: Person("Sunita", 31, "sunita@example.com")
Person(..user, age: 32)
│ │
│ └── override this field
└── copy all other fields from user
Result: Person("Sunita", 32, "sunita@example.com")
Pattern Matching on Records
Destructure a record in a case expression:
pub fn describe(person: Person) -> String {
case person {
Person(name: n, age: a, email: _) if a < 18 ->
n <> " is a minor"
Person(name: n, age: a, email: _) ->
n <> " is " <> int.to_string(a) <> " years old"
}
}Records Inside Functions
type Product {
Product(name: String, price: Float, in_stock: Bool)
}
pub fn apply_discount(p: Product, rate: Float) -> Product {
let new_price = p.price *. (1.0 -. rate)
Product(..p, price: new_price)
}
pub fn main() {
let item = Product(name: "Laptop", price: 50000.0, in_stock: True)
let sale_item = apply_discount(item, 0.10)
// sale_item.price = 45000.0
}Nested Records
type Address {
Address(street: String, city: String, pincode: String)
}
type Customer {
Customer(name: String, address: Address)
}
let customer = Customer(
name: "Rajesh",
address: Address(
street: "12 Park Road",
city: "Delhi",
pincode: "110001"
)
)
let city = customer.address.city // "Delhi"
Records vs Tuples vs Maps
Comparison Table
──────────────────────────────────────────────────
Feature │ Record │ Tuple │ Map
────────────────┼─────────┼─────────┼────────────
Named fields │ Yes │ No │ Yes (keys)
Fixed structure │ Yes │ Yes │ No
Type-checked │ Yes │ Yes │ Partial
Best for │ Entities│ Pairs │ Dynamic keys
Compile-safe │ Yes │ Yes │ Key lookups
│ │ │ return Result
Practical Example — Order System
import gleam/io
type Order {
Order(
id: Int,
item: String,
quantity: Int,
unit_price: Float
)
}
pub fn total(order: Order) -> Float {
int.to_float(order.quantity) *. order.unit_price
}
pub fn confirm(order: Order) -> String {
"Order #"
<> int.to_string(order.id)
<> ": "
<> order.item
<> " x"
<> int.to_string(order.quantity)
<> " = ₹"
<> float.to_string(total(order))
}
pub fn main() {
let o = Order(id: 1001, item: "Notebook", quantity: 5, unit_price: 120.0)
io.println(confirm(o))
// Order #1001: Notebook x5 = ₹600.0
}Key Points
Record Essentials
──────────────────────────────────────────────────
1. Define with: type Name { Name(field: Type, ...) }
2. Create with: Name(field: value, ...)
3. Access with: record.field_name
4. Update with: Name(..original, field: new_value)
5. Pattern match to destructure
6. Immutable — updates produce new records
7. Use when data has 2+ named, typed fields
Records bring structure and clarity to your data. When you see order.unit_price, you know exactly what it is — no index counting, no documentation hunting. Descriptive field names are documentation built directly into the type.
