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 = inttype Json = dict[str, str | int | float | bool | None]type ConnString = strThese 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 | Triangletype Result[T, E] = Ok[T] | Err[E]type LoadError = NotFound | Timeout | BackendThe 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 = intdoes not give you a fresh type —UserIdis interchangeable withint. 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 usetyping.TypeAlias— thetypekeyword (PEP 695 syntax) is the only form.
Emitting
Aliases emit as plain Python aliases:
type Vec[T] = list[T]type Json = dict[str, int]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 (
UserIdvsint). - You have a sealed union — the alias is the seal.
When not to alias:
- A single-character rename.
type S = stradds nothing. - A type that already has a good name.
type Map[K, V] = dict[K, V]is busywork.
Where next
- Sealed Unions — the primary use of
type X = A | B. - Generics — PEP 695 reference.