Type Aliases
Opened 2026-09-01 as a future-ergonomics RFC. Metel has no way to name a type.
type Name = T;currently exists only as an associated type inside an aspect /extendblock (RFC-0082); RFC-0039 names compound bounds (aspect Sortable = A + B + C), not types. This RFC fills the gap.
Brought into v0.13.0 (2026-09-02). Originally recorded as non-blocking. Re-scoped to co-land with RFC-0154 (the
|...|function-type spelling) and the closure cluster (RFC-0050 / RFC-0134 / RFC-0152 / RFC-0153 / RFC-0157): the qualifier grammar (once/var), its base spelling, and the tool for naming the results all ship in one migration. RFC-0154 §5 recommends parenthesizing a nested function type (|A| -> (|B| -> C)); an alias is how you write that parenthesis once. Examples below use RFC-0154's|...|form and thevarqualifier accordingly. All five Open Questions resolved 2026-09-02 — acceptance-ready.
Status — under review (2026-09-01). type-alias gap confirmed (no existing RFC); brought into v0.13.0 2026-09-02 to co-land with RFC-0154 + the closure cluster.
Status — accepted (2026-09-02). 5 OQs resolved (#88); acceptance review moved the separator to := and added the Alternatives section
Status — integrated (2026-09-02). grammar + desugar pass in metel-core feat/921; spec.declarations.type-aliases blocks added
Status — implemented (2026-09-02). module-level transparent aliases: grammar + desugar pass + fixtures in metel-core feat/921 (Tier 3 green)
Summary
A type alias is a module-level, transparent name for an existing type:
type Bytes := List<u8>;
type Handler := once var |Request, &Config| -> Response; // closure qualifiers included
type Renderer<W> := context(theme: Theme) var |W| -> Html; // parameterised; context requirement included
- Transparent (structural), not nominal.
Handlerisonce var |Request, &Config| -> Response— same type, interchangeable everywhere, no coercion, no distinct identity. RFC-0152 widening flows straight through it. - May be parameterised.
type X<A, B> := …with the same generic-parameter and bound rules as any declaration. - Names any type, including function/closure types with their
once/varqualifiers (RFC-0134 / RFC-0153), the nested function types RFC-0154 §5 recommends parenthesizing, and, once RFC-0113 lands, theircontext(...)requirement.
It does not introduce a newtype, does not add nominal identity, and does not carry a closure's capture list (see §"Function and closure types").
Motivation
- Compound types recur in signatures and drift out of sync when edited in place — the same problem RFC-0039 solves for bounds, unsolved for types.
- The closure cluster puts up to two qualifiers on a function type (
once,var), on RFC-0154's|...|base spelling.once var |Request, &Config| -> Responsewritten in every route table, struct field, and higher-order parameter is real noise, and it is exactly the kind of type an alias is best at: long, structural, repeated. - RFC-0154 §5 recommends parenthesizing a function type nested in another (
|A| -> (|B| -> C),|(|i64| -> i64)| -> String). An alias writes that parenthesis once and hands out a plain name:type Curried := |A| -> (|B| -> C);. - RFC-0113 context parameters will add a
context(...)clause to function types for the deferral case (§"Context parameters"). Without aliases,context(theme: Theme) var |Widget| -> Htmlis the worst case. - Metel deliberately has one structural function type (no per-closure nominal type), which makes a type alias here a pure synonym with zero semantic weight — the cheapest possible version of the feature.
Proposal
1. Declaration
type_alias = "public"? "type" IDENT type_params? ":=" type ";"
The separator is :=. An alias name is a kept binding — referenceable throughout its
scope — so RFC-0136's invariant puts it on :=, not =. This also matches
assoc_type_def (type Item := T;, RFC-0082), which §4 notes shares the type keyword:
the two type … := … sites now differ only by position, never by token.
Allowed at module scope or inside a function / block body (Open Question 1). public
is module-scope only. A module-level public type participates in the module's public
surface exactly like a struct / enum / fun declaration; a function-local alias is
never exported, may reference the enclosing function's generic parameters, and follows
block-local fun's scoping and shadowing rules — visible throughout its block regardless
of textual position (RFC-0131).
2. Transparency
An alias is erased during type checking to its right-hand side before any further reasoning. Consequences:
Handlerand its expansion unify, satisfy the same bounds, and are accepted in the same positions. There is noT0015-style coherence concern — an alias defines no impl.- Diagnostics prefer the alias name the user wrote (as RFC-0039 Q5 does for bound aliases), expanding it only when the mismatch is inside the expansion.
- An alias may reference another alias; expansion is recursive; cycles are a compile error (RFC-0039 Q3).
3. Parameters
type Pair<A, B> := (A, B);
type Cache<K, V> := Map<K, List<V>>;
type Predicate<T> := |T| -> bool;
Generic parameters and their bounds follow the ordinary declaration rules. Turbofish
applies to the alias, resolving through the expansion: Predicate::<i64> is |i64| -> bool. An alias with unused parameters is a warning, not an error (it may exist to keep a
family of aliases uniform).
4. Disambiguation from associated types (RFC-0082)
type Name := T; inside an aspect or extend block is an associated-type definition
(RFC-0082) and is unchanged. Anywhere else — module scope or a function / block body — it
is a type alias. The two never collide — position decides — and this RFC adds no new
keyword.
Function and closure types
This is the motivating case and the one with a subtlety.
What the alias carries. A function type in Metel is Type::Fun(params, ret, call_multiplicity, use_multiplicity, call_mutation) — the qualifiers once / var are
part of the type, so an alias captures them:
type Reducer<T> := |T, T| -> T; // many reading
type Sink<T> := var |T| -> (); // FnMut-shaped
type Consumer<T> := once |T| -> (); // FnOnce-shaped
type Middleware := |Handler| -> (|Request| -> Response); // returns a function -> §5 parens
What the alias does not carry: the capture list. [&var count, buf] is on the
closure literal, not its type (spec: "captures distinguish closure values at runtime
rather than introducing a distinct closure type"). The capture list only influences the
three multiplicity fields — use_multiplicity from whether every capture is Copy,
call_multiplicity from whether the body moves a by-value non-Copy capture,
call_mutation from whether the body mutates one — and those the alias already carries. A
type alias therefore cannot and should not embed a capture list; two closures satisfying
Sink<Event> may capture entirely different things.
To reuse a capture pattern, write a factory function:
fun counting_sink(var seen: List<Event>) -> var |Event| -> () {
[seen] var |e| -> () { seen.push(e); }
}
Where the alias still helps the literal. An alias on a let / parameter / field
ascription supplies the expected type, so the literal need not restate the
qualifiers:
type Handler := once var |Request| -> Response;
let h: Handler := [&cfg] |req| -> Response { … }; // `once var` come from `Handler`
Context parameters (RFC-0113)
RFC-0113 is amended in parallel with the function-type half of this interaction. In summary, for aliases:
- Default — the closure captures the context. A
context c: Cin scope at a closure literal is an ordinary free binding the closure closes over at creation. The resulting type is plain (|T| -> U) and an alias of it needs nocontextclause. - Deferral — a context function type.
context(c: C) |T| -> Udeferscto the closure's call site (Scala 3's context functions). This is type information, so an alias carries it:type Renderer<W> := context(theme: Theme) var |W| -> Html;. - The
context(...)clause is a row of(role, type)requirements — orthogonal to theonce/mutmultiplicity axes, following the row shape RFC-0140 already uses for its handler channel. Whether it becomes a fourthType::Funfield or stays a resolution-time-only fact (capture-only, no deferral) is RFC-0113's call; this RFC aliases whichever form results.
Relationship to existing RFCs
- RFC-0039 (aspect Alias Syntax,
1-under-review) — complementary and disjoint: RFC-0039 names compound bounds (aspect X = A + B), this names types (type X := T). RFC-0039 Q1 Option C briefly floatedtype Alias = A & B & Cas a bound-alias syntax; that is not this feature. Diagnostics-name-the-alias (RFC-0039 Q5) is adopted here too. - RFC-0082 (Associated Types,
4-implemented) —type Name := T;in aspect /extendscope is associated types, unchanged. Position disambiguates; no keyword clash. - RFC-0134 (Closure Call Capability) / RFC-0153 (Closure Mutation Axis) — supply the
once/varqualifiers an alias captures. Co-land in v0.13.0. - RFC-0152 (Function-Type Multiplicity Widening) — widening is structural, so it
applies through an alias unchanged (a
many readingclosure satisfies aHandlerparameter aliased toonce var |…|). - RFC-0113 (Context Parameters,
1-under-review, v0.13.1) — see §"Context parameters"; amended in parallel. - RFC-0154 (Pipe Notation,
4-implemented) — co-lands in v0.13.0. The alias RHS uses its|...|form; RFC-0154 §5's recommendation to parenthesize a nested function type is what makes aliases the practical tool for the returns-a-function case. - RFC-0121 (Open Rows) / RFC-0140 (Algebraic Effects) — the row shape the
context(...)clause reuses.
Alternatives considered
A nominal alias (newtype)
type Handle := i64; that is a distinct type — assignable from i64 only explicitly,
with its own impl surface. Useful, but a separate feature: it needs a coercion story, an
orphan/coherence answer, and a conversion syntax. Metel has one structural function type
by design, so the transparent synonym is the version that carries zero semantic weight and
composes with everything already. A newtype can be added later without changing this.
A distinct keyword (using / typedef / alias)
Avoids overloading type. Rejected: type … := … already exists (RFC-0082 associated
types) and §4 shows position alone disambiguates the two, so a second keyword would be
pure noise. Reusing type also means an alias reads like every other type-introducing
declaration (struct, enum).
Fold into RFC-0039's bound aliases
RFC-0039 Q1 Option C floated type Alias = A & B & C for bound aliases. Rejected there
and here: a bound (A + B, used in where / generic-param position) and a type (List<u8>,
used anywhere a type is) are different grammar positions with different well-formedness
rules. One keyword for both would need a mode switch the reader can't see. The two RFCs
stay disjoint (RFC-0039 §Relationship).
Open Questions
All resolved 2026-09-02.
- Function-body-local aliases. ✓ Resolved — allowed.
type X := T;may appear as a block statement, scoped to that block, as well as at module scope. A local alias may reference enclosing generic parameters (fun f<T>() { type Pair := (T, T); … }— a reason to have it: a module alias would need its own<T>). It cannot carrypublic(nothing local is exported). Shadowing and visibility follow block-localfun: an innertypeshadows an outer alias / type of the same name within its block, and it is visible throughout its block regardless of textual position (types have no initialisation-order hazard — the same reason RFC-0131 hoistsfun). The motivating case is the complex one-off closure type used only inside one function. - Visibility granularity. ✓ Resolved — no alias-specific design. A module-level
public typeparticipates in the public surface exactly likepublic struct/fun/enum; whatever finer visibility the module system grows, aliases inherit. A function-local alias is never exported (OQ1). - Alias in
extendtarget position. ✓ Resolved — forbidden for v0.13.0.extend MyAlias: Aspect { … }is a compile error; writeextend <the real type>: Aspect. It only matters for aliases of nominal types (type Bytes := List<u8>; extend Bytes), which reopens the orphan-rule / coherence surface for no clear gain in a first cut and hides impls from where a reader looks for them. A pure restriction — liftable later (coherence check running on the expansion) if demand appears. - Recursive aliases. ✓ Resolved — rejected, direct and mutual.
type Json = Map<String, Json>;and any cycle through a chain of aliases is a compile error (same rule as RFC-0039 Q3). A transparent alias expands eagerly and a cycle has no finite expansion; a genuinely recursive type needs astruct/enumindirection point, as in every language with transparent aliases. - Turbofish for partially-applied aliases. ✓ Resolved — no special rule.
M::<i64>binds the alias's own type parameters positionally, then expands:type M<V> := Map<String, V>makesM::<i64>the typeMap<String, i64>. The alias is another generic name; RFC-0023's ascription-vs-turbofish placement rule applies to it unchanged.
Decision
Outcome: Accepted 2026-09-02 (2-accepted, #921), opened 2026-09-01. All five
Open Questions resolved 2026-09-02 (local aliases allowed, no alias-specific visibility,
extend-on-alias forbidden for now, cycles rejected, turbofish through expansion). The
acceptance review moved the declaration separator from = to := (RFC-0136's
kept-binding invariant; matches assoc_type_def) and added the Alternatives section
(nominal newtype, a distinct keyword, folding into RFC-0039 — all rejected). 3-integrated
adds a spec.declarations.type-aliases block and worked examples covering transparency,
alias-of-alias, cycle rejection, the function-type-with-qualifiers case, and the
capture-list non-interaction; 4-implemented is grammar (type … := … at module/block
scope) + name resolution + eager expansion in the frontend.
Target: v0.13.0 — set 2026-09-02 to co-land with RFC-0154 (|...| spelling) and
the closure cluster's once / var qualifier grammar, so the function-type spelling and
the tool for naming its results ship in one migration.