Skip to content

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 | int
unionType HttpMethod = "GET" | "POST" | "PUT" | "DELETE"
unionType Shape = Circle | Rectangle | Triangle
unionType Result<T> = T | Error

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 = 42
println(describe(a)) // string:alice
println(describe(b)) // int:42

Inside the if branch the typechecker narrows id to string, so all string methods are available without an explicit cast.

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 | 8080
unionType Sign = -1 | 0 | 1
var m: HttpMethod = "GET"
// var bad: HttpMethod = "PATCH" // compile error: "PATCH" is not in HttpMethod

Literal-typed values transparently widen to their underlying primitive:

function defaultMethod(): HttpMethod { return "GET" }
var s: string = defaultMethod() // ok — "GET" widens to string

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 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.56636
println(area(new Rectangle(3.0, 4.0))) // 12

Inheritance is respected — a Dog extends Animal still matches instanceof Animal.

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.

unionType declarations are top-level and can be exported like any other symbol:

types.chuks
export unionType Id = string | int
// file: main.chuks
import { Id } from "./types"
function lookup(id: Id): string { /* ... */ return "" }

Chuks is strict about this distinction: []T | U and [](T | U) are different types, and neither is a shorthand for the other.

AnnotationMeaning
[]int | stringAn 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 ints
describeA("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 | string
function logMixed(xs: []Cell): void { /* ... */ }
  • At least two members. Single-type aliases (unionType X = string) are rejected — use the type directly, or a class / dataType if you need a nominal wrapper.
  • No recursion. unionType Tree = Leaf | Tree (direct or mutual) is rejected.
  • Structural. A named unionType Id = string | int is interchangeable with an inline string | int annotation.
  • Compile-time only. Unions produce no runtime representation. The VM and AOT backends never see the union name.
  • typeof narrowing reports the narrowed member type, not the union name.