Skip to content

Latest commit

 

History

History
186 lines (147 loc) · 7.28 KB

union-types.md

File metadata and controls

186 lines (147 loc) · 7.28 KB
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)
→ View on sorbet.run

Union types and flow-sensitivity

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

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

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 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

→ 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