Newtypes
A newtype creates a distinct type from another with no runtime cost.
The compiler refuses to mix UserId(int) with OrderId(int) even though
both are int underneath — the type is the contract.
Distinct IDs, Email with helpers, units of measure, and a factory with validation.
11-newtypes-rich.zolo
// Feature: "Rich" newtypes — opaque wrappers with helpers
// Syntax: `newtype X(T)`; create with `X.new(v)`; extract with `.unwrap()`;
// add behavior via top-level functions that take the
// newtype.
// When to use: strong domain types (Email, UserId, Money) that
// the typechecker refuses to mix with the underlying type.
use std::String
newtype UserId(int)
newtype OrderId(int)
// Distinct IDs: typechecker refuses to swap the order.
fn buy(user: UserId, order: OrderId) {
print("user {user.unwrap()} bought order {order.unwrap()}")
}
let u = UserId.new(7)
let o = OrderId.new(99)
buy(u, o)
// buy(o, u) // ERROR at compile-time: incompatible types
// Newtype with helpers — top-level functions that take the newtype.
newtype Email(str)
fn email_local(e: Email) -> str {
let parts = e.unwrap().split("@")
return parts[0]
}
fn email_domain(e: Email) -> str {
let parts = e.unwrap().split("@")
return parts[1]
}
let e = Email.new("[email protected]")
print(e.unwrap()) // [email protected]
print(email_local(e)) // alice
print(email_domain(e)) // zolo.dev
// Newtype for "units" — distinguishes Meters vs Feet.
newtype Meters(float)
newtype Feet(float)
fn meters_to_feet(m: Meters) -> Feet {
return Feet.new(m.unwrap() * 3.28084)
}
fn feet_to_meters(f: Feet) -> Meters {
return Meters.new(f.unwrap() / 3.28084)
}
let m = Meters.new(100.0)
let f = meters_to_feet(m)
print(f.unwrap()) // ~328.084
let back = feet_to_meters(f)
print(back.unwrap()) // ~100.0
// Factory with light validation — enforces an invariant.
fn make_user_id(n: int) -> UserId {
if n < 0 {
return UserId.new(0)
}
return UserId.new(n)
}
let safe = make_user_id(-3)
print(safe.unwrap()) // 0
let ok = make_user_id(42)
print(ok.unwrap()) // 42
Best practices:
- Create the value with
Type.new(v)and extract it with.unwrap(). - To add behaviour, write top-level functions that receive the newtype —
fn email_local(e: Email) -> str { ... }— rather than relying onimplover a newtype, which is not stable. - Use newtypes for IDs, units (Metres, Feet, Degrees), and any primitive value that the domain forbids from being swapped accidentally.