T1 | T2 | ... similar to TypeScript.
They allow a value to belong to one of several possible types.
Pattern matching over unions is essential for message handling.
A special case T | null is written as T? and called “nullable”.
Not only structures: arbitrary types
All these types are valid:int | sliceaddress | Point | nullIncrement | Reset | coinsint8 | int16 | int32 | int64
B | C can be passed/assigned to A | B | C | D:
match must cover all cases
In other words, it must be exhaustive.
match can be used for nullable types, since T? is T | null. It may also be used as an expression:
Auto-inference of a union results in an error
What if match arms result in different types, what is the resulting type?a is inferred as builder | int, but this is most likely not what is intended and typically indicates an error in the code.
In such situations, the compiler emits a message:
a as a union, or fix contract’s code if it’s a misprint. The same applies to other situations:
int32 | int64, which is valid, but in most cases a single integer type is expected.
The compiler shows an error, just explicitly declare a return type:
Operators is and !is
Besides match, unions can also be tested using is.
This generalizes == null; smart casts also apply:
Lazy match for unions
In all practical examples of message handling, unions are parsed withlazy:
- No union is allocated on the stack upfront; loading is deferred until needed.
matchoperates by inspecting the slice prefix (opcode), not by typeid on the stack.
lazy and comply with all type-system rules.
Read about lazy loading.
Stack layout and serialization
Unions have a complex stack layout, commonly named as “tagged unions”. Enums in Rust work the same way. Serialization depends on whetherT_i is a structure with a manual serialization prefix:
- if yes (
struct (0x1234) A), those prefixes are used - if no, the compiler auto-generates a prefix tree; for instance,
T1 | T2is called “Either”: ‘0’+T1 or ‘1’+T2