Skip to content

09 · Data Structures

Newtypes

Opaque wrappers to distinguish types that share the same physical format in the domain.

On this page

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
Playground
// 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 on impl over 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.

See also

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

enespt-br