Unions

Unions are a way to create one type that's composed of multiple different types.

Int_Or_String :: union {
    int, string
}

an_int := Int_Or_String(123)
a_string := Int_Or_String("Hello")

// Reassignment works fine
both := an_int
both = a_string

There's also an implicit cast from a variant of a union to the union

my_val: Int_Or_String
my_val = 123
my_val = "hello"

All unions include a "nil variant", unless otherwise specified. Unions store a tag to represent which variant is currently active, and 0 represents the nil variant. Variants count up from there, so 1 represents the first named type, 2 the second, etc. The type of the tag is the smallest integer that can hold all values. There is no way to customize the tag used for the union.

The tag can be read and modified directly using Reflection.

Corrupted tags are not considered UB in Odin. This means that a type switch which checks all named variants of a union does not count as exhaustive. Consider a catchall case block, or simply handle the corrupted case after the switch

If the union contains exactly one variant, which is a pointer, then the union is not tagged. Instead, the nil pointer represents the nil union. This means there is no way to represent a non-nil union holding a nil pointer. You will need to use an intermediary struct.

There is no way to provide alternate names to the variants, they are always referred to by their types.

Unions can have Type Parameters

Type Assertions

To pull a value out of a union, use a Type Assertion:

my_val := Int_Or_String(123)

as_int := my_val.(int)

If you use an incorrect type, the assertion will fail

my_val := Int_Or_String(123)

// Assertion failure, program crash
as_int := my_val.(string)

Type Assertions support optional-ok semantics, so you can do a non-crashing check using an additional variable:

my_val := Int_Or_String(123)
as_int, is_int := my_val.(int) // is_int == true
as_str, is_str := my_val.(string) // is_str == false

If the union isn't holding the asserted type, the extracted value will be that types zero value.

Type Switches

Sometimes, you want to have different behavior based on which variant the union is holding. A giant chain if two-part if/elses would get out of hand quick, so that's why there's Type Switches, which are a union version of a Switch Statement

my_val := Int_Or_String(123)

switch t in my_val {
   case int: fmt.println("It's an int!") 
   case string: fmt.println("It's a string!") 
}

Here, t is a variable representing the actual data inside the union. It's downcast to the actual type inside of a case. If you have multiple types inside of one case, t is not downcast:

switch t in something {
   case int: assert(type_of(t) == int)
   case string: assert(type_of(t) == string)
   case f32, f64: assert(type_of(t) == type_of(something))
}

fallthrough is not allowed inside a type switch.

Directives

The following directives apply to unions: