Documentation / Aliases

Aliases

Aliases give an existing thing a better name. They don't create a second value, a hidden conversion or a new nominal type.

Moth keeps as narrow: type names, imported names and arm-local payload captures.

Choose the explanation level: Type aliases

Type aliases

A type alias gives an existing type a clearer name. It does not create a new type.

When to use a type alias

Use a type alias when a type annotation would be repetitive or when a name carries domain meaning.


    UserId as Int
    HtmlFragment as String
    RouteParts as {String}
    

Aliases are transparent

An alias is another spelling of the same type.


    UserId as Int

    id UserId = 42
    raw Int = id
    

id has type UserId, but it is also an Int. You can pass it anywhere an Int is expected.

Alias versus struct

A struct creates a brand new type with its own fields and constructor. An alias does not.


    UserId as Int

    User = |
        id Int,
    |
    

UserId is Int. User is a separate type.

One collection alias

An alias can name a collection type.


    Names as {String}

    names Names = {"Priya", "Grace"}
    

A type alias is a compile-time name for an existing type. It introduces no runtime value and no new semantic type identity.

Definition and syntax


    UserId as Int
    Names as {String}
    MaybeName as String?
    StringBox as Box of String
    

The syntax is AliasName as ExistingType. Alias names follow type-name casing. The target may be any valid type expression that resolves to a concrete semantic type.

Exact contract

  • A type alias is a compile-time name for an existing type.
  • Aliases are transparent: an alias and its target denote the same TypeId.
  • Aliases do not create nominal identity.
  • Alias and target values are type-compatible in both directions.
  • Aliases are valid in type positions such as parameters, fields, returns and local annotations.
  • Aliases are not ordinary values. They cannot be assigned, passed, copied or used in expression position.
  • as in a type-alias declaration is not a cast.

Valid targets

An alias target may be:

  • a builtin scalar such as Int, Float, Bool, Char or String
  • a struct or choice declared in the same file
  • an option such as String?
  • a growable collection such as {String}
  • a fixed collection such as {4 Int}
  • a hashmap such as {String = Int}
  • a fully concrete generic instance such as Box of String
  • an imported type, imported type alias or external package type

Concrete generic aliases are supported. Parameterized aliases and partial generic application are outside the current language scope.

Transparency


    UserId as Int

    id UserId = 42
    raw Int = id
    

Because UserId is transparent, assigning a UserId value to an Int binding is valid. The reverse is also valid: an Int value can be used where UserId is expected.

Contrast with a struct


    UserId as Int

    User = |
        id Int,
    |
    

UserId is another name for Int. User is a distinct nominal struct type with its own constructor and fields. Use a struct when the domain concept needs its own identity. Use an alias when only the annotation name matters.

Empty literals and aliases

An alias can provide the explicit element type needed by an empty collection or map literal.


    Names as {String}

    names Names = {}
    

Alias chains

Aliases may chain through other aliases. The chain remains transparent and resolves to the final target type.


    Name as String
    Names as {Name}

    names Names = {"Priya"}
    

Cycles

Alias cycles are invalid. The compiler reports a circular dependency.

Aliases are not constructors

An alias does not create a new constructor. The alias name is not callable even when the alias targets a constructible nominal type. Construct the target type using its canonical nominal name.

For struct aliases, only the struct's canonical name constructs:


    Person = |
        name String,
    |

    MainPerson as Person

    -- Valid: construct with the canonical name
    person MainPerson = Person("Priya")

    -- Error: MainPerson is a type, not a constructor
    -- person MainPerson = MainPerson("Priya")
    

For choice aliases, only the choice's canonical name constructs variants:


    Status ::
        Ready,
        Failed | message String |,
    ;

    State as Status

    -- Valid: construct with the canonical name
    ready State = Status::Ready
    failed State = Status::Failed("bad")

    -- Error: State is a type, not a constructor
    -- ready State = State::Ready
    

Aliases to scalar or otherwise non-constructible targets are not callable for the same reason: the alias name is a type, not a value.

Visibility and module public surfaces

Type aliases follow ordinary module visibility rules. A type alias declared in a source file is importable by other files in the same module. Cross-module visibility is controlled by the module root's explicit export: surface. The normal module-root filename itself has no semantic role. A public re-export may expose an imported alias under a chosen public name. That operation is not the same as an ordinary local file import alias.

Related concepts

Choose the explanation level: Import aliases

Import aliases

Import aliases rename something you import so it fits better in the current file.

Two packages called render

Suppose two packages both export a function named render. You cannot import both as render because names must not collide.


    import @blog { render as render_blog }
    import @docs { render as render_docs }

    blog_html = render_blog()
    docs_html = render_docs()
    

Namespace aliases

You can also rename a whole namespace.


    import @core/math as math

    value = math.sin(1.0)
    

File-local scope

An import alias is visible only in the file where it is written. It does not change the exported name in the source file, and it is not automatically re-exported.

No shadowing

An alias cannot hide an existing name. If render is already declared in the file, import @other { render } is an error. Pick a different local name instead.

Import aliases rename an imported symbol for local use inside one file. They are distinct from type aliases and from public module re-export names.

Definition and syntax

A namespace alias changes the local name of an imported namespace:


    import @core/math as math
    

A grouped symbol alias changes the local name of one imported binding:


    import @core/math {
        sin as sine,
        PI as CIRCLE_PI,
    }
    

Exact contract

  • A namespace alias changes the namespace name visible in one file.
  • A grouped symbol alias changes one imported binding visible in one file.
  • The source declaration keeps its canonical exported identity.
  • Ordinary import aliases are file-local.
  • Ordinary import aliases are not automatically re-exported.
  • Aliases work for every importable symbol category the import surface supports: functions, constants, structs, choices, type aliases and external package members.
  • External package aliases follow the same local collision policy as source imports.
  • Each grouped entry is renamed separately.
  • A grouped import cannot use one trailing group-level alias.

Collision rules

Import aliases must not collide with any visible name in the same file, including:

  • same-file declarations
  • other imports
  • other aliases
  • type aliases
  • builtins
  • preluded names

Aliases do not shadow an existing visible name. A collision is a compile error.


    render || -> String:
        return "local"
    ;

    -- Error: render is already visible in this file
    import @other { render }
    

Resolve the collision by renaming the import.


    import @other { render as render_other }
    

Leading-case warnings

An alias whose leading case differs from the imported symbol's leading case produces a warning. The warning is intentionally simple: it catches obvious type-versus-value naming drift without trying to classify every naming convention.


    -- Warning
    import @core/math { sin as Sine }

    -- Warning
    import @core/math { PI as pi }
    

Namespace aliases

Use a namespace alias when the imported path would be awkward to repeat or when two imported namespaces share a basename.


    import @core/math as math
    import @core/text as text

    value = math.sin(1.0)
    length = text.length("hello")
    

Grouped symbol aliases

Use grouped symbol aliases when two packages export the same name.


    import @blog { render as render_blog }
    import @docs { render as render_docs }

    blog_html = render_blog()
    docs_html = render_docs()
    

A grouped import cannot use a single alias for the whole group.


    -- Error
    import @components { Button, Card } as Ui

    -- Correct
    import @components {
        Button as UiButton,
        Card as UiCard,
    }
    

Local and public-name distinction

An ordinary import alias is private to the importing file. A public name chosen in a module root's export surface is a different operation. Cross-module visibility is controlled by the module root's explicit export: surface. The normal module-root filename itself has no semantic role. The current export syntax is owned by Project Structure and Packages. Do not treat a local import alias as a public API rename.


    -- Ordinary local import alias
    import @models { User as ModelUser }
    

The local ModelUser is visible only in this file. A public rename belongs inside the module root's current explicit export: block. See Project Structure and Packages for the exact current form.

Import aliases with type aliases

A type alias may target an imported type alias.


    import @models { User as ExternalUser }

    CurrentUser as ExternalUser
    

This combines a file-local import rename with a domain-specific type alias.

Related concepts

  • For type aliases see the previous section.
  • For payload capture aliases see the next section.
  • For import syntax and module roots see Packages and Project Structure.
Choose the explanation level: Payload capture aliases

Payload capture aliases

When you match a choice with a payload, the field name becomes the local variable inside that arm. Sometimes that name collides with another variable or feels wrong in context.

Rename one payload field


    Response ::
        Err | message String |,
        Success | value String |,
    ;

    response = Response::Err("oops")

    if response is:
        Err(message as error_message) => io.line(error_message)
        Success(value as body) => io.line(body)
    ;
    

message as error_message means: take the payload field called message and bind it to error_message in this arm only.

Why rename?


    message = "outer message"

    if response is:
        Err(message as failure_message) => io.line(failure_message)
        else => io.line(message)
    ;
    

Without the alias, the capture would collide with the visible outer message and be rejected. The alias keeps both names distinct.

A payload capture alias renames a choice payload field inside one match arm. It is the third narrow as surface in Moth.

Definition and syntax


    if response is:
        Err(message as error_message) => io.line(error_message)
        Success(value as body) => io.line(body)
    ;
    

The name before as is the declared payload field name. The name after as is an arm-local binding.

Exact contract

  • The name before as must be the declared payload field name.
  • The name after as is an arm-local binding.
  • The alias does not rename the field.
  • The alias does not leak into other arms.
  • The alias is visible in the arm guard and body.
  • Without as, the field name becomes the local binding.
  • The local binding follows naming and no-shadowing rules.
  • Duplicate local bindings in the same arm are invalid.
  • Payload capture aliases do not create value aliases.
  • value as Type is not cast syntax.
  • Expression aliases are unsupported.

Avoiding outer-name collisions

Use a capture alias when the field name would collide with an existing variable or when the local meaning is clearer.


    message = "outer message"

    if response is:
        Err(message as error_message) => io.line(error_message)
        else => io.line(message)
    ;
    

Inside the first arm, error_message holds the payload. In the else arm, message still refers to the outer binding.

Related concepts

  • For choice declarations and variants see Choices.
  • For pattern matching and exhaustiveness see Branching.