Union Types
A unionType declares a named union of two or more types. Members can be
primitive types, class or dataType names, or literal values. Union types
are a compile-time feature — they are erased by the typechecker, so they
cost nothing at runtime.
unionType Id = string | intunionType HttpMethod = "GET" | "POST" | "PUT" | "DELETE"unionType Shape = Circle | Rectangle | TriangleunionType Result<T> = T | ErrorUnions of primitives
Section titled “Unions of primitives”Pair with typeof for simple branching:
unionType Id = string | int
function describe(id: Id): string { if (typeof(id) == "string") { return "string:" + id } return "int:" + string(id)}
var a: Id = "alice"var b: Id = 42println(describe(a)) // string:aliceprintln(describe(b)) // int:42Inside the if branch the typechecker narrows id to string, so all
string methods are available without an explicit cast.
Literal unions
Section titled “Literal unions”String, int, and float literals are valid members. This is the idiomatic way to model a small, closed set of values:
unionType HttpMethod = "GET" | "POST" | "PUT" | "DELETE"unionType Port = 80 | 443 | 8080unionType Sign = -1 | 0 | 1
var m: HttpMethod = "GET"// var bad: HttpMethod = "PATCH" // compile error: "PATCH" is not in HttpMethodLiteral-typed values transparently widen to their underlying primitive:
function defaultMethod(): HttpMethod { return "GET" }
var s: string = defaultMethod() // ok — "GET" widens to stringExhaustive switch on literal unions
Section titled “Exhaustive switch on literal unions”When you switch on a value whose type is a literal union, omitting a
case (and omitting default) is a compile error:
unionType HttpMethod = "GET" | "POST" | "PUT" | "DELETE"
function describe(m: HttpMethod): string { switch (m) { case "GET": { return "read" } case "POST": { return "create" } // compile error: // Switch on unionType 'HttpMethod' is not exhaustive. // Missing cases: ["DELETE" "PUT"] } return ""}Adding default or the missing cases resolves the error.
Unions of classes
Section titled “Unions of classes”Unions can mix class types. Use instanceof to narrow
inside a branch:
class Circle { public radius: float; constructor(r: float) { this.radius = r } }class Rectangle { public width: float; public height: float; constructor(w: float, h: float) { this.width = w; this.height = h } }
unionType Shape = Circle | Rectangle
function area(s: Shape): float { if (s instanceof Circle) { return 3.14159 * s.radius * s.radius // s narrowed to Circle } return s.width * s.height // s narrowed to Rectangle}
println(area(new Circle(2.0))) // 12.56636println(area(new Rectangle(3.0, 4.0))) // 12Inheritance is respected — a Dog extends Animal still matches
instanceof Animal.
Generic unions
Section titled “Generic unions”unionType Result<T> = T | Error
function parseInt(s: string): Result<int> { // ... try to parse ... if (s == "") { return new Error("empty input") } return 42}
var r: Result<int> = parseInt("42")if (r instanceof Error) { println("failed: " + r.message)} else { println("ok: " + string(r)) // r narrowed to int}Generic unions must be instantiated with type arguments at every use
site. A bare Result without <...> is a type error.
Exporting & importing
Section titled “Exporting & importing”unionType declarations are top-level and can be exported like any other
symbol:
export unionType Id = string | int
// file: main.chuksimport { Id } from "./types"
function lookup(id: Id): string { /* ... */ return "" }Arrays of unions vs. unions of arrays
Section titled “Arrays of unions vs. unions of arrays”Chuks is strict about this distinction: []T | U and [](T | U) are
different types, and neither is a shorthand for the other.
| Annotation | Meaning |
|---|---|
[]int | string | An int array, or a string. |
[](int | string) | An array whose elements are each int or string. |
The rule is simple: | has lower precedence than [], so a | outside
parentheses splits the whole annotation into union arms. To group a
union inside an array element, parenthesise it.
// []T | U — either an int-array or a scalar string.function describeA(x: []int | string): string { if (typeof(x) == "string") { return "scalar: " + x } return "array of " + string(x.length) + " ints"}
describeA([1, 2, 3]) // array of 3 intsdescribeA("hello") // scalar: hello
// [](T | U) — an array of mixed int/string elements.function tag(e: int | string): string { if (typeof(e) == "int") { return "i" + string(e) } return "s" + e}
function describeB(xs: [](int | string)): string { var out: string = "" var i: int = 0 while (i < xs.length) { out = out + tag(xs[i]) i = i + 1 } return out}
describeB([1, "two", 3, "four"]) // "i1stwoi3sfour"If you need the grouped form often, give it a name:
unionType Cell = int | stringfunction logMixed(xs: []Cell): void { /* ... */ }Rules and limitations
Section titled “Rules and limitations”- At least two members. Single-type aliases (
unionType X = string) are rejected — use the type directly, or aclass/dataTypeif you need a nominal wrapper. - No recursion.
unionType Tree = Leaf | Tree(direct or mutual) is rejected. - Structural. A named
unionType Id = string | intis interchangeable with an inlinestring | intannotation. - Compile-time only. Unions produce no runtime representation. The VM and AOT backends never see the union name.
typeofnarrowing reports the narrowed member type, not the union name.
See also
Section titled “See also”instanceof— narrowing on class unions.- Type narrowing —
== nullandtypeofnarrowing. - Classes & OOP — inheritance and interfaces.