Union types
Tolk supports union types such as T1 | T2 | T3.
A union type represents a value that can be one of several types. Pattern matching is used to distinguish union variants. A special case T | null is written as T? and referred to as a nullable type.
struct (0x12345678) Increment { /* ... */ }
struct (0x23456789) Reset { /* ... */ }
type IncomingMsg = Increment | Reset
fun handle(m: IncomingMsg) {
match (m) {
Increment => { /* here m is `Increment` */ }
Reset => { /* here m is `Reset` */ }
}
}Arbitrary union types
Union types are not limited to structures. Any types can be combined into a union. The following union types are valid:
int | slice;address | Point | null;Increment | Reset | coins;int8 | int16 | int32 | int64.
Union types are automatically flattened:
type Int8Or16 = int8 | int16
struct Demo {
t1: Int8Or16 | int32? // int8 | int16 | int32 | null
t2: int | int // int
}Union types support assignment based on subtype relations. For example, a value of type B | C can be passed to or assigned to A | B | C | D:
fun take(v: bits2 | bits4 | bits8 | bits16) {}
fun demo() {
take(someSlice as bits4); // ok
take(anotherV); // ok for `bits2 | bits16`
}Exhaustive pattern matching
A match expression must cover all possible cases. It must be exhaustive.
fun errDemo(v: int | slice | Point) {
match (v) {
slice => { v.loadAddress() }
int => { v * 2 }
// error: missing `Point`
}
}match can be used with nullable types, since T? is equivalent to T | null. It can also be used as an expression:
fun replaceNullWith0(maybeInt: int?): int {
return match (maybeInt) {
null => 0,
int => maybeInt,
}
}Union type inference errors
Auto-inference of a union type results in an error. If match arms produce values of different types, the inferred result is a union, which is typically not intended:
var a = match (...) {
... => beginCell(),
... => 123,
};The type of a is inferred as builder | int. In most cases, this indicates an error in the code. In such cases, the compiler emits the following message:
error: type of `match` was inferred as `builder | int`; probably, it's not what you expected
assign it to a variable `var a: <type> = match (...) { ... }` manuallyTo resolve this, either explicitly declare a as a union type or fix the code if the union is unintended.
The same rule applies to other cases, such as return type inference:
fun f() {
if (...) { return someInt64 }
else { return someInt32 }
}The result is inferred as int32 | int64. While it is valid, a single integer type is usually expected. The compiler reports an error. To fix it, explicitly declare the return type:
fun f(): int {
if (...) { return someInt64 }
else { return someInt32 }
}Declaring return types is recommended practice.
is and !is operators
Union types can be tested using the is operator:
fun demo(v: A | B) {
if (v is A) {
v.aMethod();
} else {
v.bMethod();
}
}lazy matching for unions
In message-handling, union values are commonly parsed using lazy:
fun onInternalMessage(in: InMessage) {
val msg = lazy MyUnion.fromSlice(in.body);
match (msg) {
// ...
}
}This pattern is referred to as lazy match:
- No union is allocated on the stack upfront; loading is deferred until needed.
matchoperates by inspecting the slice prefix (opcode), instead of checking a type identifier on the stack.
Union types continue to work correctly without lazy and follow the same type-system rules.
void as a union variant
T | void is a valid serializable union meaning "T or empty slice" — void carries no bits and does not participate in the prefix tree. This is useful for chains like wallet-v5's payload format: parse cells until refs run out. See void-never.
Stack layout and serialization
Union types have a complex stack layout, commonly referred to as tagged unions.
Serialization depends on whether the union consists of structures with manual serialization prefixes:
- if yes; for example,
struct (0x1234) A, those prefixes are used; - if no, the compiler automatically generates a prefix tree; for example,
T1 | T2is calledEithertype:0+T1or1+T2.
Last updated on