ALIASES

Aliases give a local name to something that already exists.

Beanstalk uses as for three narrow, deliberate renaming surfaces:


    UserId as Int

    import @models/User as ModelUser

    case Err(message as error_message) => io(error_message)
    

Aliases are useful when a name has a domain-specific meaning, when a type annotation is long, or when two imported libraries export symbols with the same name.

They are not a general shadowing feature. An alias introduces a visible file-local name and must not collide with any other visible name in that file.

Type Aliases

Type aliases give another name to an existing type.


    UserId as Int
    Names as {String}
    MaybeName as String?
    

The syntax is:


    AliasName as ExistingType
    

Alias names should use UpperCamelCase, the same as structs, choices, and other type names.

Type aliases are compile-time-only. They do not create runtime values and they do not emit code.

Transparent Type Aliases

Type aliases are transparent.

This means an alias does not create a new nominal type. It is another name for the target type.


    UserId as Int

    id UserId = 42
    raw Int = id -- valid, because UserId is Int
    

This is different from a struct.


    UserId as Int

    User = |
        id Int,
    |
    

UserId is just another name for Int.

User is a distinct nominal struct type.

Use a struct when you need a real domain type with its own fields, constructor, and methods. Use a type alias when you only want a clearer type name.

Type Aliases Are Not Values

A type alias can only be used where the compiler expects a type annotation.


    UserId as Int

    id UserId = 42 -- valid
    

A type alias cannot be used as a value, constructor, receiver, or copy target.


    UserId as Int

    io(UserId)      -- Error: UserId is a type alias, not a value
    id = UserId(42) -- Error: UserId is not a constructor
    copy_id = copy UserId -- Error: aliases cannot be copied
    

If the alias targets a constructible type, construct the real target type instead.


    Person = |
        name String,
        age Int,
    |

    MainPerson as Person

    person MainPerson = Person("Ada", 36)
    

Aliasing Collections and Options

Aliases can target nested type expressions.


    UserId as Int
    UserIds as {UserId}
    MaybeUserId as UserId?

    ids UserIds = {1, 2, 3}
    selected MaybeUserId = none
    

Aliases are resolved before normal type checking, so nested aliases behave exactly like their final target type.


    Name as String
    Names as {Name}

    names Names = {"Ada", "Grace", "Evelyn"}
    

Empty collection literals still need an explicit element type at the declaration site.


    Names as {String}

    names Names = {} -- valid: the alias provides the collection element type
    bad_names ~= {}  -- Error: empty collection type is ambiguous
    

Aliases in Function Signatures

Type aliases can be used in parameters, returns, and local declarations.


    UserId as Int
    Username as String

    format_user |id UserId, name Username| -> String:
        return [: #[id] [name]]
    ;

    id UserId = 7
    name Username = "Bean"

    label = format_user(id, name)
    io(label)
    

Because aliases are transparent, passing an Int where UserId is expected is valid.


    UserId as Int

    print_id |id UserId|:
        io(id)
    ;

    raw_id Int = 1
    print_id(raw_id) -- valid
    

If you need the compiler to reject raw Int values in a user-id position, use a struct instead of an alias.

Import Aliases

Import aliases rename an imported symbol for the current file.


    import @models/User as ModelUser
    import @admin/User as AdminUser

    model_user ModelUser = ModelUser("Ada")
    admin_user AdminUser = AdminUser("Grace")
    

The imported declaration keeps its original exported name in its source file. The alias only changes the name visible in the importing file.

Import aliases work for all explicitly imported symbols:


    import @core/math/sin as sine
    import @core/math/PI as CIRCLE_PI

    value = sine(CIRCLE_PI)
    

Import aliases are file-local. They are not exported and they do not rename the source declaration for other files.

Resolving Import Name Conflicts

Import aliases are most useful when two libraries export the same symbol name.


    import @blog/render as render_blog
    import @docs/render as render_docs

    blog_html = render_blog()
    docs_html = render_docs()
    

Without aliases, both imports would try to introduce render into the same file. That is a name collision.


    import @blog/render
    import @docs/render -- Error: render is already visible in this file
    

The same rule applies across value and type names. Even if the compiler can internally tell that one symbol is a type and another is a value, Beanstalk rejects duplicate visible spellings for readability.


    import @models/User
    import @formatters/User -- Error: User is already visible in this file
    

Rename one or both imports so the file remains unambiguous.


    import @models/User as ModelUser
    import @formatters/User as format_user

    user ModelUser = ModelUser("Ada")
    label = format_user(user)
    

Grouped Import Aliases

Grouped imports can rename individual imported entries.


    import @components {
        Button as PrimaryButton,
        Card as ArticleCard,
        render as render_component,
    }
    

Nested grouped paths can also use aliases.


    import @docs {
        pages/home/render as render_home,
        pages/about/render as render_about,
    }

    home = render_home()
    about = render_about()
    

The alias applies only to the final imported symbol binding. The canonical target still resolves to the full imported path.


    import @docs {
        pages/home/render as render_home,
    }

    -- The local name is render_home.
    -- The imported target is still @docs/pages/home/render.
    

A grouped import cannot use one trailing alias for the whole group. Each entry must be renamed separately.


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

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

Import Aliases and Type Aliases Together

Type aliases can target imported aliases.


    import @models/User as ExternalUser

    CurrentUser as ExternalUser

    user CurrentUser = ExternalUser("Ada")
    

This is useful when one file wants a local import name, but also wants a domain-specific type alias for annotations.

Imported type aliases can also be renamed directly.


    -- types.bst
    # UserId as Int
    

    -- #page.bst
    import @types/UserId as AccountId

    id AccountId = 42
    

Alias Collision Rules

Aliases must not collide with any visible name in the same file.

This includes:


    render ||:
        return "local"
    ;

    import @other/render as render -- Error: render is already declared in this file
    

Aliases do not shadow existing names. Pick a more specific name instead.


    import @other/render as render_other
    

Re-importing the same symbol with a different alias should be avoided unless the file genuinely needs both names. Prefer one local spelling per imported symbol.

Alias Naming Warnings

Import aliases should keep the same broad naming convention as the imported symbol.

If the imported symbol starts with a capital letter, the alias should also start with a capital letter. If the imported symbol starts with a lowercase letter, the alias should also start with a lowercase letter.


    import @models/User as ModelUser      -- good
    import @models/User as model_user     -- warning

    import @formatters/render as render_page -- good
    import @formatters/render as RenderPage  -- warning
    

This warning is intentionally simple. It does not try to fully distinguish camelCase,

snake_case, or other naming styles.

The goal is only to catch obvious type/value naming drift.

Choice Payload Capture Aliases

Choice payload capture aliases rename a matched payload field inside one match arm.


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

    response = Response::Err("Missing title")

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

The name before as is the declared payload field name. The name after as is the local capture name visible inside that arm.


    case Err(message as error_message) => io(error_message)
    

This keeps the pattern tied to the declared payload shape while still letting the arm choose a clearer local name.

Without as, the capture name is the field name.


    case Err(message) => io(message)
    

Capture aliases are useful when a field name would collide with an existing variable or when the local meaning is clearer than the generic payload field name.


    message = "outer message"

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

Choice payload capture aliases are arm-local. They do not rename the variant field itself and they do not leak into other arms.

What as Does Not Do

as is not a cast operator and it is not a general renaming expression.

These forms are invalid:


    value = input as Int        -- Error: use explicit casts such as Int(input)
    renamed as existing_value   -- Error: this is not a value alias
    function_call() as result   -- Error: expression aliases are not supported
    

Use as only for supported alias declarations, import aliases, and Choice payload capture aliases.

This keeps renaming predictable and avoids adding multiple ways to express the same idea.

When To Use Aliases

Use a type alias for clearer annotations:


    UserId as Int
    HtmlFragment as String
    CssClass as String
    RouteParts as {String}
    

Use an import alias when an imported name is too generic or conflicts with another visible name:


    import @blog/render as render_blog
    import @docs/render as render_docs
    

Use a Choice payload capture alias when the field name is correct on the type but awkward in one arm:


    case Failed(message as failure_message) => io(failure_message)
    

A good rule: