Sorbet

Sorbet

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

›Experimental Features

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

RBS Comments Support

Signature comments

This feature is experimental and might be changed or removed without notice. To enable it pass the --enable-experimental-rbs-comments option to Sorbet or add it to your sorbet/config.

Sorbet has experimental support for comment-only type syntax, powered by RBS annotations.

The syntax looks like this:

#: (Integer) -> String
def foo(x)
  T.reveal_type(x) # Revealed type: `Integer`

  x.to_s
end

str = foo(42)
T.reveal_type(str) # Revealed type: `String`

This feature is powered by translating RBS comments to equivalent sig syntax. For example, the previous example is similar to having written a sig directly:

sig { params(x: Integer).returns(String) }
def foo(x)
  ...
end

Long signatures can be broken into multiple lines using the #| continuation comment:

#: (
#|  Integer,
#|  String
#| ) -> Float
def foo(x, y); end

Caveats

Support for this feature is experimental, and we actively discourage depending on it for anything other than to offer feedback to the Sorbet developers.

There are numerous shortcomings of the comment-based syntax versus sig syntax. By contrast, the near-sole upside is that the syntax is terser.

The comment-based syntax is second class

The comment-based syntax uses RBS syntax. RBS is an alternative annotation syntax for Ruby. The headline features of RBS:

  • It’s the type annotation syntax blessed by the Ruby core team, and Matz in particular (the lead of the Ruby project).
  • RBS supports certain enticing features, like duck typing.
  • The syntax is terse.
  • The Ruby distribution bundles a gem for parsing RBS annotations.

However, there are a number of problems which mean that RBS syntax has a second-class position in Sorbet:

RBS syntax does not match the semantics of Sorbet

Sorbet’s type annotation syntax evolved differently from the evolution of RBS syntax. Unfortunately, this difference is not syntax-deep: it affects the semantics of the types too.

Sure, there are similarities: both syntaxes offer a way to express union types, for example. But there are also semantic differences, and RBS syntax reflects these differences. Things which are possible to express in RBS syntax have no analogue in Sorbet and vice versa. Some examples:

  • RBS supports duck typing via interfaces (different from Sorbet’s interfaces), but Sorbet does not support duck typing, by design.
  • With Sorbet all class singleton classes are generic (T.class_of(...)[...]). RBS does not have syntax to represent this.
  • By extension, Sorbet allows singleton classes to declare their own generic type parameters (with type_template). This also cannot be translated from RBS for the same limitation with RBS’s singleton class type annotation.
  • RBS supports literal value types. Sorbet does not.

Because of these differences, it’s reasonable to assume that a codebase wishing to take full advantage of Sorbet’s unique features will eventually need to have annotations that use sig syntax. The moment a method’s annotation needs to use Sorbet-only syntax, the entire annotation needs to get rewritten—it’s not possible to embed Sorbet-only syntax within the context of an RBS signature. While it is possible for RBS comment signatures to coexist with Sorbet sig signatures, needing to flip between them adds development friction.

Sorbet has minimal influence over the evolution of RBS syntax

Sorbet continues to evolve its type syntax. For example, T.anything, T::Class, and has_attached_class! are additions to Sorbet which arrived 6 years after Sorbet’s inception—Sorbet development is anything but stale!

Sorbet can adapt more quickly to the needs of Sorbet users by building on its own syntax. Historically, it has been difficult to advocate to adding RBS features that benefit Sorbet specifically.

Our belief is that syntax matters less than semantics; that “what’s possible to express in the type system” matters more than “how to express it.” Time spent bikeshedding syntax detracts from meaningful improvements to the type system.

IDE integration is more difficult

Sorbet’s type syntax doubles as Ruby syntax. Setting aside other benefits of reusing Ruby syntax for type annotations (e.g., runtime checking, no need to transpile, seamless integration with linters, etc.), Sorbet’s syntax integrates easily with existing editors.

There are a lot of features that come for free by reusing Ruby syntax:

  • Syntax highlighting

    Highlighting for RBS comment-based type annotations is provided through the Ruby LSP. Without it, they will be monochromatic, highlighted like comments.

  • Syntax error tolerance and recovery

    The RBS comment parser does not recover from syntax errors. If there’s a syntax error in an RBS comment, Sorbet will ignore the entire sig until the error is fixed.

  • Autocompletion

    Without error tolerance, autocompletion support is unreliable.

In fairness, these are technical, implementation considerations, and thus could hope to improve one day. But they remain problems today.

Performance is worse

The chosen implementation for RBS annotations in comments is:

  1. Scan and parse the file, like normal.
  2. Using the parsed file contents, re-scan the file, looking for comments, because Sorbet does not feed parsed comments throughout its pipeline.
  3. Parse the RBS comments, using a third-party RBS parser, which manages memory allocations using it’s own internal data structures.
  4. Translate the parsed RBS types to equivalent Ruby ASTs, and splice those ASTs into the parse result.
  5. Allow Sorbet to continue, where signatures and type annotations will be analyzed later in the pipeline.
┌──────────────┐
│  scan+parse  │
└──────┬───────┘
       ├──────────────────────────┐
       │                          ▼
       │                   ┌──────────────┐
       │                   │  scan again  │
       │                   └──────┬───────┘
       │                          ▼
       │                   ┌─────────────┐
       │                   │  parse RBS  │
       │                   └──────┬──────┘
       │                          ▼
       │                   ┌─────────────┐
       │                   │  translate  │
       │                   └─────────────┘
       ├──────────────────────────┘
       ▼
┌────────────────┐
│  ingest types  │
└────────────────┘

This implementation is simple, making it easy to verify correctness. The RBS parser can be developed as a library and tested independently. As long as the translation produces Sorbet ASTs equivalent to sig annotations that a user would have written, ingesting the types will work correctly.

But steps 2 and 4 are pure overhead (step 3 is a wash). Not only does this implementation scan every file which might use RBS comments twice, but it does not parse straight into Sorbet’s internal type representation.

In large codebases, this adds nontrivial overhead, and is a blocker in the way of being able to advocate for using this syntax more widely.

Runtime checking is a feature

Note that runtime checking of RBS signatures is not implemented, so type safety is reduced.

Sorbet’s signatures are not just static type annotations: they are also checked at runtime. Runtime-checked signatures are a key reason why Sorbet type annotations are so accurate: authors can’t “lie” when writing type annotations.

For more information on why runtime checking is valuable, see here:

→ Runtime type checking is great

In fact, runtime checking for signatures is actually load bearing: simply disabling runtime checking can make the code change behavior when run.

The Sorbet docs will continue using Sorbet syntax

There are no plans to rewrite the Sorbet website to present RBS alternatives alongside the existing Sorbet syntax. In the mean time, users will need to mentally translate from Sorbet syntax to RBS syntax on their own, including understanding the cases where there is no suitable RBS replacement.

Quick reference

Most RBS features can be used and will be translated to equivalent Sorbet syntax during type checking:

RBS FeatureRBS syntaxSorbet syntax

Class instance type

Foo
Foo

Class singleton type

singleton(Foo)
T.class_of(Foo)

Union type

Foo | Bar
T.any(Foo, Bar)

Intersection type

Foo & Bar
T.all(Foo, Bar)

Optional type

Foo?
T.nilable(Foo)

Untyped type

untyped
T.untyped

Boolean type

bool
T::Boolean

Nil type

nil
NilClass

Top type

top
T.anything

Bottom type

bot
T.noreturn

Void type

void
void

Generic type

Foo[Bar]
Foo[Bar]

Generic method

[U] (U foo) -> U
type_parameters(:U)
  .params(foo: T.type_parameter(:U))
  .returns(T.type_parameter(:U))

Tuple type

[Foo, Bar]
[Foo, Bar]

Shape type

{ a: Foo, b: Bar }
{ a: Foo, b: Bar }

Proc type

^(Foo) -> Bar
T.proc.params(arg: Foo).returns(Bar)

Block type

{ (Foo) -> Bar }
T.proc.params(arg: Foo).returns(Bar)

Optional Block type

?{ (Foo) -> Bar }
T.nilable(T.proc.params(arg: Foo).returns(Bar))

Attribute accessor types

Attribute accessors can be annotated with RBS types:

#: String
attr_reader :foo

#: Integer
attr_writer :bar

#: String
attr_accessor :baz

Long attribute types can span over multiple lines:

#: [
#|   Integer,
#|   String
#| ]
attr_reader :foo

Method annotations

While RBS does not support the same modifiers as Sorbet, it is possible to specify them using @ annotation comments.

The following signatures are equivalent:

# @override
#: (Integer) -> void
def bar1(x); end

sig { override.params(x: Integer).void }
def bar2(x); end

# @override(allow_incompatible: true)
#: (Integer) -> void
def baz1(x); end

sig { override(allow_incompatible: true).params(x: Integer).void }
def baz2(x); end

# @final
#: (Integer) -> void
def qux1(x); end

sig(:final) { params(x: Integer).void }
def qux2(x); end

Note: these annotations like @override use normal comments, like # @override (not the special #: comment). This makes it possible to reuse any existing YARD or RDoc annotations.

While the @abstract method annotation is technically supported, it is not recommended. Read more about abstract methods below.

Class and module annotations

RBS annotations can be used to add Sorbet helpers to classes like abstract!:

# @abstract
class Foo; end
end

This is equivalent to:

class Foo
  extend T::Helpers

  abstract!
end

The @interface!, @final!, and @sealed! annotations are supported in the same way.

The @requires_ancestor annotation expects an argument to represent the ancestor to require:

# @requires_ancestor: ::Some::Ancestor
class Foo; end

This is equivalent to:

class Foo
  extend T::Helpers

  requires_ancestor { Some::Ancestor }
end

Generic classes and modules

RBS supports generic classes and modules.

Type members

Type members can be specified using a #: comment on the class or module:

#: [E]
class Box
  #: -> void
  def initialize
    @elems = [] #: T::Array[E]
  end

  #: (E) -> void
  def <<(e)
    @elems << e
  end
end

box = Box.new #: Box[Integer]
box << 42

RBS generics do not use T::Generic, thus the [] method doesn’t exist at runtime and Sorbet will report an error if you try to use it:

Box[Integer].new
   ^^^^^^^^^ error: Method `[]` does not exist on `T.class_of(Box)`

Because of this, Sorbet will use the RBS comment type as the type of the class being instantiated:

box = Box.new #: Box[Integer]
T.reveal_type(box) # Revealed type: `Box[Integer]`

To change the type of the class being instantiated, the expression can be broken into multiple lines:

nilable_box = Box #: Class[Box[Integer]]
   .new #: Box[Integer]?
T.reveal_type(nilable_box) # => `T.nilable(Box[Integer])`

1.times do
  nilable_box = nil
end

You can also use an intermediate variable to change the type of the class being instantiated:

box_of_integer = Box.new("foo") #: Box[Integer]
T.reveal_type(box_of_integer) # => `Box[Integer]`
nilable_box = box_of_integer #: as Box[Integer]?
T.reveal_type(nilable_box) # => `T.nilable(Box[Integer])`

1.times do
  nilable_box = nil
end

Type templates

There is no equivalent to type_template in RBS. Instead, you can define type members on the singleton class. So this:

module Factory
  extend T::Sig
  extend T::Generic

  InstanceType = type_template

  sig { returns(InstanceType) }
  def self.make; end
end

Can be written as:

module Factory
  #: [InstanceType]
  class << self
    #: -> InstanceType
    def make; end
  end
end

Note: there is no RBS equivalent to the generic T.class_of()[] syntax just yet.

Variance

Variance can be specified using the in and out keywords:

#: [out E]
class Box
end

box = Box.new #: Box[Integer]
box #: Box[Numeric]

Bounds

By default, type parameters do not have bounds:

#: [E]
class Box; end

box = Box.new #: Box[Integer]

Upper bounds can be specified using <:

#: [E < Numeric]
class Box; end

Box.new #: Box[Integer]
Box.new #: Box[String]
               ^^^^^^ error: `String` is not a subtype of upper bound of type member `::Box::E`

Fixed bounds can be specified using =:

#: [E = Integer]
class Box; end

Note: the lower bound > syntax is not supported in RBS yet.

Special behaviors

The #: comment must come immediately before the following method definition. If there is a blank line between the comment and method definition, the comment will be ignored.

Generic types like Array or Hash are translated to their T:: Sorbet types equivalent:

  • Array[Integer] is translated to T::Array[Integer]
  • Class[Integer] is translated to T::Class[Integer]
  • Enumerable[Integer] is translated to T::Enumerable[Integer]
  • Enumerator[Integer] is translated to T::Enumerator[Integer]
  • Enumerator::Lazy[Integer] is translated to T::Enumerator::Lazy[Integer]
  • Enumerator::Chain[Integer] is translated to T::Enumerator::Chain[Integer]
  • Hash[String, Integer] is translated to T::Hash[String, Integer]
  • Range[Integer] is translated to T::Range[Integer]
  • Set[Integer] is translated to T::Set[Integer]

Note that non-generic types are not translated, so Array without a type argument stays Array.

Unsupported features

Abstract methods

While the @abstract annotation is technically supported for methods, it is currently not recommended. To understand why, take the following example:

# @abstract
class Foo
  # @abstract
  # -> void
  def foo; end
end

class Bar < Foo
  # @override
  # -> void
  def foo
    super
  end
end

When using traditional Sorbet sigs, the call to super inside of Bar#foo would error at runtime, because sorbet-runtime uses metaprogramming to remove the Foo#foo method. Any mistaken calls to this method would raise an error at runtime.

However, RBS signatures have no runtime component. This means that any inadvertent calls to abstract methods will silently no-op, which has the potential to create subtle bugs and unintended behavior in production.

Instead of using the @abstract annotation on methods, place any abstract methods in a separate .rbi shim file.

Modifying the example above, leave only the abstract class definition in the Ruby file, along with the definition of its subclass.

Note: the class Foo must still be defined in a Ruby file. Without this definition, Foo will not exist at Runtime, and the program will error when referencing the Foo class.

# @abstract
class Foo; end

class Bar < Foo
  # @override
  # -> void
  def foo
    super
  end
end

Then, in a separate shim file, add any abstract methods:

class Foo
  sig { abstract.returns(String) }
  def foo; end
end

Using this approach, the abstract method will be visible to Sorbet during static type checking, but it will not exist at runtime, resulting in obvious errors if any child classes attempt to call it.

Class types

The class type in RBS is context sensitive (depends on the class where it is used) and Sorbet does not support this feature yet. Instead, use the equivalent Sorbet syntax:

class Foo
  sig { returns(T.attached_class) }
  def self.foo; end
end

Interface types

Interface types are not supported, use the equivalent Sorbet syntax instead:

module Foo
  extend T::Helpers

  interface!
end

#: (Foo) -> void
def takes_foo(x); end

Alias types

Alias types are not supported, use the equivalent Sorbet syntax instead:

Bool = T.type_alias { T::Boolean }

sig { params(x: Bool).void }
def foo(x); end

Literal types

Sorbet does not support RBS’s concept of "literal types". The next best thing is to use the literal’s underlying type instead:

  • 1 is Integer
  • "foo" is String
  • :foo is Symbol
  • true is TrueClass
  • false is FalseClass
  • nil is NilClass

You can also consider using T::Enum.

Unchecked generics

RBS unchecked generics are not supported by Sorbet, use untyped instead:

#: [E]
class Box; end

box = Box.new #: Box[untyped]

Type assertions comments

This feature is experimental and might be changed or removed without notice. To enable it pass the --enable-experimental-rbs-comments option to Sorbet or add it to your sorbet/config.

T.let assertions

T.let assertions can be expressed using RBS comments:

x = 42 #: Integer
@x = 42 #: Integer
X = 42 #: Integer

This is equivalent to:

x = T.let(42, Integer)
@x = T.let(42, Integer)
X = T.let(42, Integer)

The comment must be placed at the end of the assignment. Either on the same line if the assignment is on a single line, or on the last line if the assignment spans multiple lines:

x = [
  1, 2, 3
] #: Array[Integer]

The only exception is for HEREDOCs, where the comment may be placed on the first line of the HEREDOC:

X = <<~MSG #: String
  foo
MSG

T.cast assertions

T.cast assertions can be expressed using RBS comments with the as keyword:

x = 42 #: as Integer

This is equivalent to:

x = T.cast(42, Integer)

The comment is always applied to the outermost expression. For example, in this method call, the cast is applied to the value returned by foo and not the argument x:

foo x #: as Integer

This is equivalent to:

T.cast(foo x, Integer)

It is possible to cast the argument x by using parentheses:

foo(
  x #: as Integer
)

This is equivalent to:

foo(T.cast(x, Integer))

Casts comments can be used in any context where a type assertion is valid:

foo
  .bar #: as Integer
  .baz

[
  foo, #: as Integer
  {
    bar: baz, #: as String
  }
]

x = if foo
  bar
else
  baz
end #: as Integer

T.must assertions

T.must are denoted with the special as !nil comment:

foo #: as !nil

This is equivalent to:

T.must(foo)

The as !nil comment can be used in any context where a cast is valid and follows the same rules as as Type comments:

foo(
  x #: as !nil
)

T.unsafe escape hatch

T.unsafe can be replaced with the special as untyped annotation:

x = 42 #: as untyped
x.undefined_method # no error statically, but will fail at runtime

This is equivalent to:

x = T.unsafe(42)
x.undefined_method

T.absurd

T.absurd can be expressed using RBS comments:

#: (Integer | String) -> void
def foo(x)
  case x
  when Integer
  when String
  else
    x #: absurd
  end
end

This is equivalent to:

T.absurd(x)

T.bind

T.bind can be expressed using RBS comments with the special self as construct:

class Foo
  def foo; end
end

def bar
  #: self as Foo

  foo
end

This is equivalent to:

def bar
  T.bind(self, Foo)

  foo
end
← Requiring Ancestors
  • Signature comments
  • Caveats
    • The comment-based syntax is second class
    • RBS syntax does not match the semantics of Sorbet
    • Sorbet has minimal influence over the evolution of RBS syntax
    • IDE integration is more difficult
    • Performance is worse
  • Quick reference
  • Attribute accessor types
  • Method annotations
  • Class and module annotations
  • Generic classes and modules
    • Type members
    • Type templates
    • Variance
    • Bounds
  • Special behaviors
  • Unsupported features
    • Abstract methods
    • Class types
    • Interface types
    • Alias types
    • Literal types
    • Unchecked generics
  • Type assertions comments
    • T.let assertions
    • T.cast assertions
    • T.must assertions
    • T.unsafe escape hatch
    • T.absurd
    • T.bind

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