Skip to main content

Type Aliases

Type aliases give names to existing types, making code more readable and self-documenting. They are transparent: an alias does not create a new nominal type, it names a structural shape.

Basic Syntax​

The type keyword creates a type alias:

type UserId = Nat;
type Amount = Int;
type Coordinate = (Int, Int);

After declaration, UserId and Nat are interchangeable. The alias adds semantic meaning without creating a distinct runtime or proof type.

Common Use Cases​

Primitive Wrappers​

Give semantic names to primitive types:

type-alias-primitive.sr9
// Type aliases for domain concepts
type UserId = Nat;
type Balance = Nat;
type Timestamp = Int;

persistent actor {
var nextId : UserId = 0;
var totalBalance : Balance = 0;

public func createUser() : async UserId
modifies nextId
ensures result == old(nextId);
ensures nextId == old(nextId) + 1;
{
let id = nextId;
nextId += 1;
id
};

public func deposit(amount : Balance) : async ()
modifies totalBalance
ensures totalBalance == old(totalBalance) + amount;
{
totalBalance += amount;
};
}

Record Types​

Name complex record structures:

type-alias-record.sr9
// Record type aliases for structured data
type Point = { x : Int; y : Int };
type Rectangle = { topLeft : Point; bottomRight : Point };

persistent actor {
transient var position : Point = { x = 0; y = 0 };

public func move(dx : Int, dy : Int) : async ()
modifies position
ensures position.x == old(position.x) + dx;
ensures position.y == old(position.y) + dy;
{
position := { x = position.x + dx; y = position.y + dy };
};

pure func width(rect : Rectangle) : Int {
rect.bottomRight.x - rect.topLeft.x
};

pure func height(rect : Rectangle) : Int {
rect.bottomRight.y - rect.topLeft.y
};
}

Variant Types​

Create named variant types for state machines and enums:

type-alias-variant.sr9
// Variant type aliases for enums and state machines
type Status = { #pending; #approved; #rejected };
type Result<T> = { #ok : T; #err : Text };

persistent actor {
transient var status : Status = #pending;

public func approve() : async ()
modifies status
ensures status == #approved;
{
status := #approved;
};

public func reject() : async ()
modifies status
ensures status == #rejected;
{
status := #rejected;
};

pure func isComplete(s : Status) : Bool {
switch (s) {
case (#pending) { false };
case (#approved) { true };
case (#rejected) { true };
}
};

pure func unwrapOr<T>(r : Result<T>, default : T) : T {
switch (r) {
case (#ok(value)) { value };
case (#err(_)) { default };
}
};
}

Function Types​

Simplify higher-order function signatures:

type-alias-function.sr9
// Function type aliases for higher-order code
type UnaryOp = Int -> Int;
type BinaryOp = (Int, Int) -> Int;

module Math {
public pure func apply(f : UnaryOp, x : Int) : Int
ensures result == f(x);
{
f(x)
};

public pure func negate() : UnaryOp {
pure func(x : Int) : Int { -x }
};

public pure func double() : UnaryOp {
pure func(x : Int) : Int { x * 2 }
};
}

Module Types​

Name module interfaces for reuse:

type-alias-module.sr9
// Module type aliases for interface reuse
type Processor = module {
process : Nat -> Nat;
};

module Fast {
public func process(n : Nat) : Nat
ensures result == n * 2;
{ n * 2 };
};

module Slow {
public func process(n : Nat) : Nat
ensures result == n + 1;
{ n + 1 };
};

module Selector {
// Type alias makes the return type clearer
public func pick(useFast : Bool) : Processor {
if (useFast) Fast else Slow
};
};

Generic Type Aliases​

Type aliases can have type parameters:

type Pair<A, B> = (A, B);
type Box<T> = { value : T };
type Result<T, E> = { #ok : T; #err : E };

Use them by providing type arguments:

let coords : Pair<Int, Int> = (10, 20);
let wrapped : Box<Text> = { value = "hello" };
let outcome : Result<Nat, Text> = #ok(42);

Bounded Type Parameters​

Constrain type parameters with upper bounds using <::

type Named<T <: { name : Text }> = { item : T; count : Nat };

The bound restricts T to types with at least a name field. Without an explicit bound, parameters default to Any.

Visibility​

Type aliases follow normal visibility rules:

module Types {
// Public: usable outside this module
public type UserId = Nat;

// Private: only usable within this module
type InternalId = Int;
};

Recursive Types​

Type aliases can reference themselves for recursive data structures:

type Tree = {
value : Int;
left : ?Tree;
right : ?Tree
};

type List<T> = ?(T, List<T>);

The verifier handles recursive types through normalization and cycle detection, but the type definition must remain productive.

Verification Behavior​

Type aliases are transparent to verification. The verifier normalizes aliases to their underlying types, so:

  • Proof obligations use the expanded form
  • Pattern matching over aliased variants works correctly
  • Mutable record aliases share permission predicates when aliased

For example, these are equivalent for verification:

type Status = { #pending; #done };
var state : Status = #pending;

// Same as:
var state : { #pending; #done } = #pending;

Restrictions​

Several patterns are rejected:

Non-Productive Types​

Type definitions must have a valid base case:

// Rejected: no base case
type Bad<T> = Bad<T>;

// Rejected: circular without base
type A<T> = B<T>;
type B<T> = A<T>;

Expansive Types​

Type parameters must appear as immediate arguments, not wrapped in larger expressions:

// Accepted: T is immediate
type Good<T> = ?Good<T>;

// Rejected: T wrapped in optional
type Bad<T> = ?Bad<?T>;

// Rejected: T in record
type Also_Bad<T> = { #nil; #cons : (T, Also_Bad<{ tag : T }>) };

Cyclic Bounds​

Type parameter bounds cannot form cycles:

// Rejected: self-cycle
func f<A <: A>(a : A) { };

// Rejected: mutual cycle
func g<A <: B, B <: A>(a : A, b : B) { };

Open Type Definitions​

Types cannot capture parameters from outer scopes:

// Rejected: D captures A from outer scope
class C<A>() {
type D = A;
};

Best Practices​

  1. Use aliases for domain concepts: type Balance = Nat is clearer than raw Nat
  2. Name function types: type Comparator<T> = (T, T) -> Int improves readability
  3. Group related types: Place type aliases in dedicated modules for organization
  4. Prefer aliases over inline types: Named types in signatures are easier to understand

Summary​

  • type Name = ... creates a type alias
  • Aliases are interchangeable with their underlying types
  • Generic aliases use <T> syntax with optional <: bounds
  • Recursive types work with proper base cases
  • The verifier expands aliases transparently
  • Non-productive, expansive, and cyclic definitions are rejected