Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Type aliases

A type alias gives an existing type another name. Use one to shorten a long type that you write often, such as HashMap<String, Array<int>>, or to say what a value means, such as Score instead of int.

Declaring an alias

Write type, the new name, =, and the type it stands for:

# An alias is another name for an existing type.
type Score = int;
type Names = Array<String>;
type Handler = (int) => int;

struct Player {
    var name: String;
    var best: Score = 0;        # aliases work in fields...
}

type Team = Player;

double_it(x: int): int {
    return x * 2;
}

# ...and in parameters and return types.
apply(h: Handler, x: Score): Score {
    return h(x);
}

main(): int {
    var s: Score = 21;
    var plain: int = s;         # Score and int are the same type
    assert(apply(double_it, plain) == 42, "function type alias");

    var names = Names();        # construct through the alias
    names.append("Ada");
    assert(names.count == 1, "Names is Array<String>");

    var p = Team();
    p.best = s;
    var q: Player = p;          # Team and Player are the same type
    assert(q.best == 21, "struct alias");
    return 0;
}

An alias is only a spelling, not a new type. The compiler replaces every use of the alias with its target before it checks anything else, so Score and int are interchangeable: you can assign one to the other, pass one where the other is expected, and error messages name the target type.

You can use an alias anywhere a type can appear: local and module-level variables, parameters, return types, struct fields, generic arguments, and casts. An alias of a struct also works where the struct’s name would: to construct it (Team()) and to call its static methods.

Generic aliases

An alias can have type parameters of its own, including defaults, just like a generic struct:

# Generic aliases take their own type parameters.
type Predicate<T> = (T) => bool;
type Lookup<V> = HashMap<String, V>;
type Outcome<T, E=Error> = Result<T, E>;

is_even(x: int): bool {
    return x % 2 == 0;
}

count_matching(values: Array<int>, test: Predicate<int>): int {
    var n = 0;
    for (var v in values) {
        if (test(v)) {
            n += 1;
        }
    }
    return n;
}

parse_digit(text: String): Outcome<int> {
    if (text == "7") {
        return Outcome<int>.ok(7);
    }
    return Outcome<int>.err(Error("not a digit I know"));
}

main(): int {
    var values = Array<int>();
    values.append(1);
    values.append(2);
    values.append(4);
    assert(count_matching(values, is_even) == 2, "Predicate<int> is (int) => bool");

    var ages = Lookup<int>();   # HashMap<String, int>
    ages.insert("Ada", 36);
    assert(ages.contains("Ada"), "Lookup<int> is HashMap<String, int>");

    # Outcome<int> fills E with its default: Result<int, Error>.
    assert(parse_digit("7").value() == 7, "ok");
    assert(parse_digit("x").is_err(), "err");
    return 0;
}

The arguments at the use site fill the alias’s parameters in order, and omitted ones take their defaults. Outcome<int> becomes Result<int, Error>, and Lookup<int> becomes HashMap<String, int>. Aliases are especially handy for function types, which are long to write out: Predicate<int> reads better than (int) => bool.

Aliases across files

Like other top-level declarations, an alias is private to the file (or module) that declares it. Mark it export to let files that import it use it; see Visibility and Modules and imports.

Recursive aliases

An alias must not refer to itself, directly or through other aliases. The compiler reports the cycle at the alias, with a note at each alias it passes through:

type Celsius = Degrees;
type Degrees = Celsius;
main.pep:1:6: error: type alias 'Celsius' refers to itself
    1 | type Celsius = Degrees;
      |      ^~~~~~~
main.pep:2:6: note: through type alias 'Degrees', declared here
    2 | type Degrees = Celsius;
      |      ^~~~~~~