Skip to content

Type Aliases

A type alias gives a name to another type. It is transparent — the alias is interchangeable with what it aliases. No new type is created.

Plain alias

type UserId = int
type Json = dict[str, str | int | float | bool | None]
type ConnString = str

These are pure readability tools. The checker substitutes through them:

def find(id: UserId) -> str?:
...
let u = find(42) # ✅ int is assignable to UserId (same type)

Generic alias

type Vec[T] = list[T]
type Pair[A, B] = tuple[A, B]
type Lookup[K, V] = dict[K, list[V]]
type Maybe[T] = T?

The parameters bind at the use site:

let xs: Vec[int] = [1, 2, 3] # same as list[int]
let p: Pair[str, int] = ("a", 1) # same as tuple[str, int]

Aliases for sealed unions

The most common use:

type Shape = Circle | Rectangle | Triangle
type Result[T, E] = Ok[T] | Err[E]
type LoadError = NotFound | Timeout | Backend

The type keyword on the left is what makes the union sealed for exhaustiveness purposes (see Sealed Unions).

Recursive aliases (v0.13.0)

A type alias may now refer to itself, so self-describing shapes like JSON are expressible directly:

type Json = None | bool | int | float | str | list[Json] | dict[str, Json]

tyc::cyclic_type_alias only fires for a cycle with no type constructor anywhere — a cycle with no base case, like type Loop = Loop or type A = B; type B = A. A cycle that passes through a container or a non-alias leaf (as Json does through list[...] / dict[...] and the scalar members) has a base case and is legal.

Container literals resolve their element expectations through aliases and unions, so a nested Json literal checks all the way down:

let doc: Json = {"name": "ada", "tags": ["x", "y"], "meta": {"n": 1}} # ✅

Quoted (forward-reference) annotations (v0.13.0)

A string-literal annotation resolves as a forward reference to the type it names — useful for self-referential classes and types defined later in the file:

class Node:
value: int
next: "Node"
parent: "Node"? # nullable forward reference (v1.0.0-beta.1)
def grow() -> "Tree[T]": ...
let xs: "list[Node]" = []

The ? suffix applies to a quoted reference like any other type expression, so parent: "Node"? is how a self-referential class spells its optional back-pointer. "Node?" — with the ? inside the quotes — means the same thing and was the only accepted spelling before v1.0.0-beta.1.

This is distinct from the literal-singleton union form (type Color = "red" | "green"), which is unchanged: there the strings are values in a string-literal union, not forward references to named types.

What aliases are not

  • Not new nominal types. type UserId = int does not give you a fresh type — UserId is interchangeable with int. If you want a distinct wrapper (for accidental-confusion safety), use a small class:

    class UserId:
    value: int
  • Not generic constraints. type Vec[T: Ordered] = list[T] parses but the constraint is partial in the current release. For now, put the bound on the consumer function.

  • Not the same as TypeAlias. Typhon does not use typing.TypeAlias — the type keyword (PEP 695 syntax) is the only form.

Emitting

Aliases emit as plain Python aliases:

type Vec[T] = list[T]
type Json = dict[str, int]

PEP 695 introduces type X = ... as a Python-native form on 3.12+. The emitter passes it through unchanged.

When to alias

  • The type is long and repeats (dict[str, list[tuple[int, int, int]]]).
  • The name communicates intent better than the structure (UserId vs int).
  • You have a sealed union — the alias is the seal.

When not to alias:

  • A single-character rename. type S = str adds nothing.
  • A type that already has a good name. type Map[K, V] = dict[K, V] is busywork.

Where next