type Name = Target gives an existing type a second source name. Alias names
use PascalCase.
struct User { id: int, name: string }
type UserId = int
type Users = []User
fn first_id(us: Users): UserId {
return match us.get(0) {
Some(u) => u.id
None => 0
}
}
fn f(): int {
return first_id([User { id: 7, name: "ada" }])
}Transparency
A plain alias creates no new type. UserId and int are the same type in
both directions, so values cross freely and no conversion is needed.
type Score = int
fn best(xs: []Score): Score {
mut m := 0
for s in xs {
if s > m { m = s }
}
return m
}
fn f(): int {
raw: []int = [1, 5, 3]
return best(raw) + 1
}raw is a []int and best declares []Score; they unify because the alias
expanded. The same applies to the result — Score arithmetic is int
arithmetic.
An alias of an alias expands recursively, and two aliases of the same target are mutually assignable:
type Meters = float
type Kilometers = float
fn travel(d: Meters): Kilometers {
return d / 1000.0
}
fn f(): float {
km: Kilometers = travel(2500.0)
m: Meters = km
return m
}If that last assignment looks wrong to you, it is — the alias documents intent
but enforces nothing. Use distinct (below) when the compiler should refuse.
What can be aliased
Any type expression. Aliases pay off most on the shapes that are tedious to retype: nested collections, function types, and instantiated generics.
struct Request { path: string }
struct Response { code: int }
type Handler = fn(Request) -> Response
type Routes = Map[string, Handler]
fn dispatch(routes: Routes, r: Request): int {
return match routes.get(r.path) {
Some(handler) => handler(r).code
None => 404
}
}
fn f(): int {
mut routes: Routes = Map.new()
routes.put("/", r => Response { code: 200 })
return dispatch(routes, Request { path: "/" })
}type Point = (float, float)
type Path = []Point
type Label = string?
fn describe(p: Path, l: Label): string {
return "${l ?? "unnamed"} has ${p.len()} points"
}
fn f(): string {
return describe([(0.0, 0.0), (1.0, 1.0)], None)
}Fallible results are aliased through Result, not through the ! signature
sugar — type Loaded = int ! LoadError does not parse.
error LoadError { NotFound }
type Loaded[T] = Result[T, LoadError]
fn load(id: int): Loaded[string] {
if id < 0 { return Err(LoadError.NotFound) }
return Ok("ada")
}
fn f(): string {
return load(1).unwrap_or("?")
}Generic aliases
Type parameters go in square brackets and every use must supply the declared arity.
type Pair[A, B] = (A, B)
type Lookup[T] = Map[string, T]
type Predicate[T] = fn(T) -> bool
fn keep(xs: []int, p: Predicate[int]): []int {
return xs.filter(p)
}
fn f(): int {
entry: Pair[string, int] = ("port", 8080)
mut cache: Lookup[int] = Map.new()
cache.put(entry.0, entry.1)
return keep([1, 2, 3], v => v > 1).len() + (cache.get("port") ?? 0)
}Each substitution is independent: Pair[int, bool] and Pair[float, string]
have nothing to do with each other.
Parameters can carry bounds, which become obligations checked wherever concrete arguments expand the alias.
type Ordered[T: Comparable[T]] = []T
fn smallest[T: Comparable[T]](xs: Ordered[T]): T? {
return xs.get(0)
}
fn f(): int {
return smallest([3, 1, 2]) ?? 0
}A bound on an alias is not behavior the alias implements. It only records what the target needs.
Cycles
An alias must eventually expand to a real type. A cycle is reported when the alias is used.
type First = Second
type Second = First
fn f(x: First): int { return 0 }That reports ATOLL1019: type alias First refers to itself (recursive aliases are not allowed). Genuine recursion belongs in a struct or enum whose recursive
field goes through a managed container — see
Enums.
Distinct aliases
Prefix the declaration with distinct to get a new nominal type that keeps the
target’s representation. Values of the representation no longer satisfy it.
distinct type OrderId = u64
fn take_order(id: OrderId): void {}
fn f(): void {
raw: u64 = 42
take_order(raw)
}Two distinct aliases over the same representation are also separate, which is the whole point:
struct Raw { v: int }
distinct type Meters = Raw
distinct type Feet = Raw
fn add(a: Meters, b: Meters): int { return a.v + b.v }
fn f(m: Meters, ft: Feet): int {
return add(m, ft)
}A distinct alias over a struct still exposes that struct’s fields, so it works
as a units-of-measure wrapper without an extra layer of .inner:
struct Raw { v: int }
distinct type Meters = Raw
distinct type Feet = Raw
fn Meters.doubled(self): int {
return self.v * 2
}
fn compare(a: Meters, b: Meters): bool {
return a.v < b.v
}
fn f(a: Meters, b: Meters): int {
if compare(a, b) { return a.doubled() }
return b.doubled()
}Constructing a distinct value
A distinct alias over a struct is constructed through the alias name, which is what gives the value its new identity:
struct Raw { v: int }
distinct type Meters = Raw
fn Meters.doubled(self): int {
return self.v * 2
}
fn main(): void {
m := Meters { v: 3 }
println("${m.doubled()}")
}Constructing it through the representation does not work — Raw { v: 1 } is a
Raw, and that is the entire point of the declaration:
struct Raw { v: int }
distinct type Meters = Raw
fn meters(v: int): Meters {
return Raw { v: v }
}A distinct alias over a scalar currently has no construction path at all.
Neither an annotation nor an as cast produces one, so distinct type OrderId = u64 declares a type no program can create a value of:
distinct type OrderId = u64
fn main(): void {
id: OrderId = 42
println("${id}")
}Two live values of a struct-backed distinct alias in the same function also
fail to compile — the pair reaches the backend and is rejected with ATOLL5005: WasmIR could not lower continuation, even though the identical code over the
plain struct builds. Until that is fixed, keep at most one distinct value alive
per function and pass the others as parameters.
Note the fn Meters.doubled(self) receiver-prefix form: a distinct alias has
its own method identity, and those methods do not leak onto the representation
type or onto a sibling distinct alias.
A distinct alias over an anonymous union does accept member injection, so it is directly constructible:
struct Cash { amount: int }
struct Card { last4: string }
distinct type Payment = Cash | Card
distinct type Refund = Cash | Card
fn charge(p: Payment): int {
return match p {
Cash(c) => c.amount
Card(_) => 0
}
}
fn f(): int {
p: Payment = Cash { amount: 250 }
return charge(p)
}Payment and Refund have identical members and are still different types —
without distinct they would collapse to the same canonical union.
Choosing
| You want | Use |
|---|---|
| a shorter name for a long type | transparent type |
| domain vocabulary with no enforcement | transparent type |
| the compiler to reject a mixed-up unit | distinct type, or a one-field struct |
| fields, methods, construction, evolution | struct |
| a named set of alternatives | enum |
Changing a public transparent alias’s target is a source and ABI change for every consumer, because they were coupled to the expanded type all along. Changing a distinct alias’s representation keeps the nominal identity but still moves the layout.
A composed example
Aliases doing what they are best at: naming the three shapes an index module passes around, so the signatures read in domain terms.
struct User { id: int, name: string, active: bool }
type UserId = int
type Users = []User
type UserIndex = Map[UserId, User]
fn build(people: Users): UserIndex {
mut idx: UserIndex = Map.new()
for u in people {
if u.active {
idx.put(u.id, u)
}
}
return idx
}
fn name_of(idx: UserIndex, id: UserId): string {
return match idx.get(id) {
Some(u) => u.name
None => "unknown"
}
}
fn roster(idx: UserIndex, ids: []UserId): string {
mut out := ""
for id in ids {
out += name_of(idx, id) + " "
}
return out
}
fn f(): string {
people: Users = [
User { id: 1, name: "ada", active: true },
User { id: 2, name: "bob", active: false },
User { id: 3, name: "cy", active: true },
]
idx := build(people)
return roster(idx, [1, 2, 3])
}Every alias here is transparent, so [1, 2, 3] is a valid []UserId and
idx.get(id) returns a plain User?. The names cost nothing at runtime and
make the three-signature module readable at a glance.