Skip to content

Latest commit

 

History

History
173 lines (137 loc) · 6 KB

union-types.md

File metadata and controls

173 lines (137 loc) · 6 KB
id title sidebar_label
union-types
Union Types
Union Types (T.any)

Union types declare that a value either has one type, or some other type. The basic syntax for T.any is:

T.any(SomeType, SomeOtherType, ...)

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)
→ View on sorbet.run

Union types and flow-sensitivity

Given a value x with a type like T.any(Integer, String), Sorbet will only allow calls to methods that both types have in common, like this:

sig {params(x: T.any(Integer, String)).void}
def example(x)
  # both `Integer` and `String` have a `to_s` method, so this is okay
  puts(x.to_s)
end

But sometimes we want to be able to call a method that only exists on one of those two types. For example, Integer has an even? method that doesn't exist on String. If we didn't do anything special, Sorbet would report an error:

sig {params(x: T.any(Integer, String)).void}
def example(x)
  # ERROR: Method `even?` does not exist on `String` component of `T.any(Integer, String)`
  x.even?
end

In situations like these, we have to first check whether x is an Integer or not, and only then call the method:

sig {params(x: T.any(Integer, String)).void}
def example(x)
  if x.is_a?(Integer)
    x.even? # OK, because we checked with `is_a?`
  end
end

Sorbet is smart enough to understand many different kinds of Ruby control flow constructs (more than just if statements and calls to is_a?). Read the flow-sensitive typing section for a deeper dive on this topic.

Enumerations

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

→ View on sorbet.run

In cases like this where the classes in the union don't actually carry 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

→ View on sorbet.run

T.nilable and T::Boolean

T.nilable and T::Boolean are both defined in terms of T.any:

  • T.nilable(x) is a type constructor that will return T.any(NilClass, x)
  • T::Boolean is a type alias to T.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
→ View on sorbet.run