Union types
The type of a variable or expression can consist of multiple types. This is called a union type. For example, when assigning to a same variable inside different if branches:
if 1 + 2 == 3
a = 1
else
a = "hello"
end
a # : Int32 | String
At the end of the if, a
will have the Int32 | String
type, read “the union of Int32 and String”. This union type is created automatically by the compiler. At runtime, a
will of course be of one type only. This can be seen by invoking the class
method:
# The runtime type
a.class # => Int32
The compile-time type can be seen by using typeof:
# The compile-time type
typeof(a) # => Int32 | String
A union can consist of an arbitrary large number of types. When invoking a method on an expression whose type is a union type, all types in the union must respond to the method, otherwise a compile-time error is given. The type of the method call is the union type of the return types of those methods.
# to_s is defined for Int32 and String, it returns String
a.to_s # => String
a + 1 # Error, because String#+(Int32) isn't defined
If necessary a variable can be defined as a union type at compile time
# set the compile-time type
a = 0.as(Int32|Nil|String)
typeof(a) # => Int32 | Nil | String
Union types rules
In the general case, when two types T1
and T2
are combined, the result is a union T1 | T2
. However, there are a few cases where the resulting type is a different type.
Union of classes and structs under the same hierarchy
If T1
and T2
are under the same hierarchy, and their nearest common ancestor Parent
is not Reference
, Struct
, Int
, Float
nor Value
, the resulting type is Parent+
. This is called a virtual type, which basically means the compiler will now see the type as being Parent
or any of its subtypes.
For example:
class Foo
end
class Bar < Foo
end
class Baz < Foo
end
bar = Bar.new
baz = Baz.new
# Here foo's type will be Bar | Baz,
# but because both Bar and Baz inherit from Foo,
# the resulting type is Foo+
foo = rand < 0.5 ? bar : baz
typeof(foo) # => Foo+
Union of tuples of the same size
The union of two tuples of the same size results in a tuple type that has the union of the types in each position.
For example:
t1 = {1, "hi"} # Tuple(Int32, String)
t2 = {true, nil} # Tuple(Bool, Nil)
t3 = rand < 0.5 ? t1 : t2
typeof(t3) # Tuple(Int32 | Bool, String | Nil)
Union of named tuples with the same keys
The union of two named tuples with the same keys (regardless of their order) results in a named tuple type that has the union of the types in each key. The order of the keys will be the ones from the tuple on the left hand side.
For example:
t1 = {x: 1, y: "hi"} # Tuple(x: Int32, y: String)
t2 = {y: true, x: nil} # Tuple(y: Bool, x: Nil)
t3 = rand < 0.5 ? t1 : t2
typeof(t3) # NamedTuple(x: Int32 | Nil, y: String | Bool)