Skip to content

Aliases

Name an existing type with `type`, parameterize the alias, and use `distinct` when the new name should be a new type.

Updated View as Markdown

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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close