id | title |
---|---|
union-types |
Union Types |
Union types are how we merge the values of a set of types into one new type. The
basic syntax for T.any
is:
T.any(SomeType, SomeOtherType, ...)
Note that T.any
requires at least two type arguments.
For example, T.any(Integer, String)
describes a type whose values can be
either Integer
or String
values, but no others.
class A
extend T::Sig
sig {params(x: T.any(Integer,String)).void}
def self.foo(x); end
end
# 10 and "Hello, world" both have type `T.any(Integer, String)`
A.foo(10)
A.foo("Hello, world")
# error: Expected `T.any(Integer, String)` but found `TrueClass`
A.foo(true)
Information can be learned about union types by examining its values in
conditionals, or the case
statement. For example, when using Object#is_a?
in
a conditional, Sorbet will learn information and propagate it down to branches
of the conditional:
class A
extend T::Sig
sig {params(x: T.any(String, Integer, TrueClass)).void}
def foo(x)
T.reveal_type(x) # Revealed type: T.any(String, Integer, T::Boolean)
if x.is_a?(String) || x.is_a?(Integer)
T.reveal_type(x) # Revealed type: T.any(String, Integer)
else
T.reveal_type(x) # Revealed type: TrueClass
end
end
end
Similarly, any classes specified in the when
clause of a case
statement will
update Sorbet's knowledge of the types within the body of that branch:
class A
extend T::Sig
sig {params(x: T.any(String, Integer, TrueClass)).void}
def foo(x)
T.reveal_type(x) # Revealed type: T.any(String, Integer, TrueClass)
case x
when Integer, String
T.reveal_type(x) # Revealed type: T.any(Integer, String)
else
T.reveal_type(x) # Revealed type: TrueClass
end
end
end
Read the flow-sensitive typing section for a deeper dive on this topic.
Union types can be used to express enumerations. For example, if we have three
classes A
, B
, and C
, and would like to make one type that describes these
three cases, T.any(A, B, C)
is a good option:
class A; end
class B; end
class C;
extend T::Sig
sig {void}
def bar; end
end
class D
extend T::Sig
sig {params(x: T.any(A, B, C)).void}
def foo(x)
x.bar # error: method bar does not exist on A or B
case x
when A, B
T.reveal_type(x) # Revealed type: T.any(B, A)
else
T.reveal_type(x) # Revealed type: C
x.bar # OK, x is known to be an instance of C
end
end
end
In cases like this where the classes in the union don't actually cary around any extra data, Sorbet has an even more convenient way to define enumerations. See Typed Enumerations via T::Enum.
Note that enumerations using primitive or literal types is not supported. For example, the following is not valid:
class A
extend T::Sig
sig { params(input_param: T.any('foo', 'bar')).void }
def a(input_param)
puts input_param
end
end
T.nilable
and T::Boolean
are both defined in terms of T.any
:
T.nilable(x)
is a type constructor that will returnT.any(NilClass, x)
T::Boolean
is a type alias toT.any(TrueClass, FalseClass)
An effect of this implementation choice is that the same information propagation behavior outlined in Union types and flow sensitivity will take place for nilable types and booleans, as with any other union type:
class A
extend T::Sig
sig {params(x: T.nilable(T::Boolean)).void}
def foo(x)
if x.nil?
T.reveal_type(x) # Revealed type: NilClass
else
T.reveal_type(x) # Revealed type: T::Boolean
if x
T.reveal_type(x) # Revealed type: TrueClass
else
T.reveal_type(x) # Revealed type: FalseClass
end
end
end
end