Sorbet

Sorbet

  • Get started
  • Docs
  • Try
  • Community
  • GitHub
  • Blog

›Type System

Getting Started

  • Overview
  • Adopting Sorbet
  • Tracking Adoption
  • Quick Reference
  • Visual Studio Code
  • TypeScript ↔ Sorbet

Static & Runtime

  • Gradual Type Checking
  • Enabling Static Checks
  • Enabling Runtime Checks
  • RBI Files
  • CLI Quickstart
  • CLI Reference
  • Runtime Configuration

Troubleshooting

  • Troubleshooting
  • Why type annotations?
  • FAQ
  • Error Reference
  • Unsupported Ruby Features

Type System

  • sig
  • Type Annotations (non-sig)
  • T.let, T.cast, T.must, T.bind
  • Class Types (Integer, String)
  • Arrays & Hashes
  • Nilable Types (T.nilable)
  • Union Types (T.any)
  • Flow-Sensitivity (is_a?, nil?)
  • T.type_alias
  • Exhaustiveness (T.absurd)
  • T::Struct
  • T::Enum
  • T.untyped
  • Blocks, Procs, & Lambdas
  • Abstract Classes & Interfaces
  • Final Methods & Classes
  • Override Checking
  • Sealed Classes
  • T.class_of
  • T.self_type
  • T.noreturn
  • T.anything
  • T.attached_class
  • Intersection Types (T.all)
  • Generics
  • T::NonForcingConstants
  • Banning untyped

Editor Features

  • Language Server (LSP)
  • Server Status
  • LSP & Typed Level
  • Go to Definition
  • Hover
  • Autocompletion
  • Find All References
  • Code Actions
  • Outline & Document Symbols
  • Documentation Comments
  • Suggesting sigs
  • Highlighting untyped
  • sorbet: URIs

Experimental Features

  • Tuples
  • Shapes
  • Overloads
  • Requiring Ancestors
  • RBS Comments
Edit

Union Types

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

  # ERROR, intentionally unsupported
  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

← Nilable Types (T.nilable)Flow-Sensitivity (is_a?, nil?) →
  • Union types and flow-sensitivity
  • Enumerations
  • T.nilable and T::Boolean

Get started · Docs · Try · Community · Blog · Twitter