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.assert_type!, 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
  • Banning untyped

Ruby & DSL Features

  • attr_reader
  • minitest

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

Type Aliases

Alias = T.type_alias {Type}

This creates a type alias of Type called Alias. In the context of Sorbet, the type alias has exactly the same behavior as the original type and can be used anywhere the original type can be used. The converse is also true.

Note that the type alias will not show up in error messages.

# typed: true
extend T::Sig

Int = T.type_alias {Integer}
Str = T.type_alias {String}

sig { params(x: Int).returns(Str) }
def foo(x)
  T.reveal_type(x) # Revealed type: Integer
  x.to_s
end

a = T.let(3, Integer)
foo(a)
b = T.let(3, Int)
foo(b)

c = foo(3)
T.reveal_type(c) # Revealed type: String

When creating a type alias from another type alias, you must use T.type_alias again:

A = T.type_alias {Integer}
B = T.type_alias {A}

For simple use cases, type aliases are nearly identical to just making a new constant:

# typed: true
extend T::Sig

A = T.type_alias {Integer}
sig { returns(A) }
def foo; 3; end

B = Integer
sig { returns(B) }
def bar; 3; end

However, when the type is more complex, you must use type aliases:

# typed: true
extend T::Sig

A = T.type_alias {T.any(Integer, String)}
sig { returns(A) }
def foo; 3; end

B = T.any(Integer, String)
sig { returns(B) } # error: Constant B is not a class or type alias
def bar; 3; end

Note that because type aliases are a Sorbet construct, they cannot be used in certain runtime contexts. For instance, it is not possible to match an expression against a type alias in a case expression.

# typed: true
extend T::Sig

class A; end
class B; end
class C; end

AB = T.type_alias {T.any(A, B)}
sig { params(x: T.any(AB, C)).returns(Integer) }
def invalid(x) # error: Returning value that does not conform to method result type
  case x
  when AB then 1 # <- this line is problematic
  when C then 2
  end
end

We could refactor this example to use A, B in the when and AB in the sig. However, this introduces coupling between the definition of AB and our method. If we ever updated the definition of AB, we would need to update the definition of our method as well.

sig { params(x: T.any(AB, C)).returns(Integer) }
def valid(x)
  case x
  when A, B then 1
  when C then 2
  end
end

T.type_alias and RBI files

Type aliases defined in RBI files do not exist at runtime because RBI files are not loaded by the Ruby VM. For this reason, it’s usually dangerous to put T.type_alias declarations in RBI files, because Sorbet would not catch code that actually uses it (e.g., inside a runtime-checked sig).

To define a type alias that doesn’t participate in runtime checking, a better alternative is to define the type alias in a Ruby source file with .checked(:never):

MyType = T.type_alias { T.any(Integer, String) }.checked(:never)

See below for more information on .checked in type aliases.

.checked: Controlling runtime checking

Like .checked on method signatures, type aliases allow configuring whether the type alias participates in runtime checking:

# (1) Always validate (default)
MyType = T.type_alias { T.any(Integer, String) }.checked(:always)

# (2) Only validate in tests
MyType = T.type_alias { T.any(Integer, String) }.checked(:tests)

# (3) Never validate at runtime
MyType = T.type_alias { T.any(Integer, String) }.checked(:never)

Note: The .checked goes outside of the { ... } block, unlike with sig, where it goes inside the block.

When a type alias is not checked (either .checked(:never), or .checked(:tests) outside of a test environment), the type alias accepts all values at runtime, similar to T.anything. Like with .checked on a sig, Sorbet always uses the real type when doing static checks, regardless of the runtime checked level.

When omitted, type aliases use the T::Configuration.default_checked_level, which defaults to :always. See .checked on method signatures for more. Note in particular that .checked(:tests) requires special setup, documented there.

The .checked annotation only affects runtime value checking: the actual type defined inside the block will still be used for things like runtime override checking.

What about type aliases for method signatures?

Sometimes a question arises like, “Is there a way to factor an entire method signature into a type alias, not just types for individual arguments?”

No, there is not. This is mostly for simplicity of implementation within Sorbet.

Two workarounds are:

  1. Define type aliases for all argument and return types of the methods in question.
  2. Factor shared arguments into a typed data structure (perhaps using T::Struct), and update the methods in question to take that structure.

Note that types for lambdas and procs can be written in type aliases using proc types.

What about recursive type aliases?

Some languages have recursive type aliases. For example, TypeScript allows writing type aliases like this one which vaguely describes the type of all JSON documents (example uses TypeScript syntax):

type JSON = null | number | string | JSON[] | {[arg: string]: JSON};

Sorbet does not support recursive type aliases. To have types that reference themselves, use class types.

class SelfReferential
  extend T::Sig

  sig { returns(T.nilable(SelfReferential)) }
  attr_reader :val

  sig { params(val: T.nilable(SelfReferential)).void }
  def initialize(val); @val = val; end
end

Unfortunately for the case of typing JSON, this generally leads to more verbosity than in other languages, but can still accomplish something similar:

→ Full example on sorbet.run

For the specific example of typing JSON, note that most Sorbet users tend to just use T::Hash[String, T.untyped] or T.untyped. Serializing and deserializing JSON is usually handled better by purpose-built serialization libraries. The type of “all JSON documents” is usually unnaturally wide—it’s better to have an explicit step which converts the loosely JSON data structure into a more structured internal representation.

← Flow-Sensitivity (is_a?, nil?)Exhaustiveness (T.absurd) →
  • T.type_alias and RBI files
  • .checked: Controlling runtime checking
  • What about type aliases for method signatures?
  • What about recursive type aliases?

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