id | title | sidebar_label |
---|---|---|
faq |
Frequently Asked Questions |
FAQ |
Sorbet implements a flow-sensitive type system, but there are some limitations. In particular, Sorbet does not assume a method called twice returns the same thing each time!
See Limitations of flow-sensitivity for a fully working example.
Sorbet uses RBI files to annotate the types for the Ruby standard library. Every RBI file for the Ruby standard library is maintained by hand. This means they're able to have fine grained types, but it also means that sometimes they're incomplete or inaccurate.
The Sorbet team is usually too busy to respond to requests to fix individual bugs in these type annotations for the Ruby standard library. That means there are two options:
-
Submit a pull request to fix the type annotations yourself.
Every RBI file for the Ruby standard library lives here in the Sorbet repo. Find which RBI file you need to edit, and submit a pull request on GitHub with the changes.
This is the preferred option, because then every Sorbet user will benefit.
-
Use an escape hatch to opt out of static type checks.
Use this option if you can't afford to wait for Sorbet to be fixed and published. (Sorbet publishes new versions to RubyGems nightly).
If you're having problems making a change to Sorbet, we're happy to help on Slack! See the Community page for an invite link.
sig {void}
Sorbet has special knowledge for attr_reader
, attr_writer
, and
attr_accessor
. To add types to methods defined with any of these helpers, put
a method signature above the declaration, just like any other method:
# typed: true
class A
extend T::Sig
sig {returns(Integer)}
attr_reader :reader
sig {params(writer: Integer).returns(Integer)}
attr_writer :writer
# For attr_accessor, write the sig for the reader portion.
# (Sorbet will use that to write the sig for the writer portion.)
sig {returns(Integer)}
attr_accessor :accessor
sig {void}
def initialize
@reader = T.let(0, Integer)
@writer = T.let(0, Integer)
@accessor = T.let(0, Integer)
end
end
Because Sorbet knows what
attr_*
methods define what instance variables, intyped: strict
there are some gotchas that apply (which are the same for all other uses of instance variables): in order to use an instance variable, it must be initialized in the constructor, or be marked nilable (T.nilable(...)
).
Array
is the built-in Ruby class for arrays. On the other hand,
T::Array[...]
is a Sorbet generic type for arrays. The ...
must always be
filled in when using it.
While Sorbet implicitly treats Array
the same as T::Array[T.untyped]
, Sorbet
will error when trying to use T::Array
as a standalone type.
→ Generics in the Standard Library
When defining initialize
for a class, we strongly encourage that you use
.void
. This reminds people instantiating your class that they probably meant
to call .new
, which is defined on every Ruby class. Typing the result as
.void
means that it's not possible to do anything with the result of
initialize
.
Your method should accept BasicObject
and return T::Boolean
.
Unfortunately, not all BasicObject
s have is_a?
, so we have to do one extra
step in our ==
function: check whether Object === other
. (In Ruby, ==
and
===
are completely unrelated. The latter has to do with case subsumption).
The idiomatic way to write Object === other
in Ruby is to use case
:
case other
when Object
# ...
end
Here's a complete example that uses case
to implement ==
:
Hash#[]
and Array#[]
return a nilable type because in
Ruby, accessing a Hash or Array returns nil
if the key does not exist or is
out-of-bounds. If you would rather raise an exception than handle nil
, use the
#fetch
method:
[0, 1, 2].fetch(3) # IndexError: index 3 outside of array bounds
You might notice this when calling Array#sample
, Pathname#find
, or other
stdlib methods that accept a keyword argument and can have different return
types based on arguments:
T.reveal_type([1, 2, 3].sample) # Revealed type: T.nilable(T.any(Integer, T::Array[Integer]))
The sig in Sorbet's stdlib is quite wide, since it has to cover every possible return type. Sorbet does not have good support for this for methods that accept keyword arguments. #37 is the original report of this. #2248 has an explanation of why this is hard to fix in Sorbet.
To work around this, you'll need to use T.cast
.
item = T.cast([1, 2, 3].sample, Integer)
T.reveal_type(item) # Revealed type: Integer
In some cases - for example, with complex number conversion methods in
Kernel
- the Sorbet team has chosen to ship technically incorrect RBIs that
are much more pragmatic. See #1144
for an example. You can do the same for other cases you find annoying, but you
take on the risk of always need to call the method correctly based on your new
sig.
For example, if you are confident you'll never call Array#sample
on an empty
array, use this RBI to not have to worry about nil
returns.
class Array
extend T::Sig
sig do
params(arg0: Integer, random: Random::Formatter)
.returns(T.any(Elem, T::Array[Elem]))
end
def sample(arg0=T.unsafe(nil), random: T.unsafe(nil)); end
end
Or if you never call it with an argument (you always do [1,2,3].sample
, never
[1,2,3].sample(2)
), use this RBI to always get an element (not an array) as
your return type:
class Array
extend T::Sig
sig do
params(arg0: Integer, random: Random::Formatter)
.returns(Elem) # or T.nilable(Elem), to also support empty arrays
end
def sample(arg0=T.unsafe(nil), random: T.unsafe(nil)); end
end
Overriding stdlib RBIs can make type checking less safe, since Sorbet will now have an incorrect understanding of how the stdlib behaves.
Another alternative is to define new methods that are stricter about arguments, and use these in place of stdlib methods:
class Array
extend T::Sig
sig { returns(Elem) } # or T.nilable(Elem) unless you're confident this is never called on empty arrays
def sample_one
T.cast(sample, Elem)
end
sig { params(n: Integer).returns(T::Array[Elem]) }
def sample_n(n)
T.cast(sample(n), T::Array[Elem])
end
end
Sorbet can't know what the "parent method" is 100% of the time. For example,
when calling super
from a method defined in a module, the super
method will
be determined only once we know which class or module this module has been mixed
into. That's a lot of words, so here's an example:
To typecheck this example, Sorbet would have to typecheck MyModule#foo
multiple times, once for each place that method might be used from, or place
restrictions on how and where this module can be included.
Sorbet might adopt a more sophisticated approach in the future, but for now it
falls back to treating super
as a method call that accepts anything and
returns anything.
Kind of, with some effort. Rake monkey patches the global main
object (i.e.,
top-level code) to extend their DSL, which Sorbet cannot understand:
# -- from lib/rake/dsl_definition.rb --
...
# Extend the main object with the DSL commands. This allows top-level
# calls to task, etc. to work from a Rakefile without polluting the
# object inheritance tree.
self.extend Rake::DSL
Sorbet cannot model that a single instance of an object (in this case main
)
has a different inheritance hierarchy than that instance's class (in this case
Object
).
To get around this, factor out all tasks defined the Rakefile
that should be
typechecked into an explicit class in a separate file, something like this:
# -- my_rake_tasks.rb --
# (1) Make a proper class inside a file with a *.rb extension
class MyRakeTasks
# (2) Explicitly extend Rake::DSL in this class
extend Rake::DSL
# (3) Define tasks like normal:
task :test do
puts 'Testing...'
end
# ... more tasks ...
end
# -- Rakefile --
# (4) Require that file from the Rakefile
require_relative './my_rake_tasks'
For more information, see this StackOverflow question.
Sorbet has not reached version 1.0 yet. As such, it will make breaking changes constantly, without warning.
To upgrade a patch level (e.g., from 0.4.4314 to 0.4.4358):
bundle update sorbet sorbet-runtime
# also update plugins like sorbet-rails, if any
# For plugins like sorbet-rails, see their docs, eg.
https://github.com/chanzuckerberg/sorbet-rails#initial-setup
# Optional: Suggest new, stronger sigils (per-file strictness
# levels) when possible. Currently, the suggestion process is
# fallible, and may suggest downgrading when it's not necessary.
bundle exec srb rbi suggest-typed
The sorbet-runtime
gem is currently only tested on Ruby 2.6 and Ruby 2.7. It
is known to not support Ruby 2.4. Feel free to report runtime issues for any
current or future Ruby version.
The sorbet-static
gem is known to support Ruby 2.4, Ruby 2.5, Ruby 2.6, and
Ruby 2.7 to a minimum level (i.e., it can at least parse syntax introduced in
those versions). Some language features are typed more strictly than others
(generally, language features in newer Ruby versions have looser type support).
This is not by design, just by convenience. Feel free to open feature requests
that various (new or old) language features be typed more strictly.
Sorbet bundles RBI files for the standard library. In Ruby the standard library changes with the Ruby version being used, but Sorbet only ships one set of RBI definitions for the standard library. In particular, Sorbet's RBI files for the standard library might reflect classes, methods, or APIs that are only available in a version of Ruby newer than the one used to run a given project. You will have to rely on (runtime) test suites to verify that your project does not use new standard library APIs with an old Ruby version.
The sorbet-static
gem is only tested on macOS 10.14 (Mojave) and Ubuntu 18
(Bionic Beaver). There is currently no Windows support. We expect
sorbet-static
to work as far back as macOS 10.10 (Yosemite), as far forward as
macOS 11.0 Big Sur, and on most Linux distributions using glibc
.
We do not test nor publish prebuilt binaries for macOS on Apple Silicon. We have reports that it doesn't work, but no one on the Sorbet team has access to Apple Silicon-based macOS machines, so we have been unable to diagnose nor fix any problems. If you are interested in working on this, feel free to reach out in the #internals channel on our Sorbet Slack.
The sorbet
gem has runtime dependencies on git
and bash
.
Combined, these points mean that if you are using one of the official minimal Ruby Docker images (which are based on Apline Linux), you will need to install some support libraries:
FROM ruby:2.6-alpine
RUN apk add --no-cache --update \
git \
bash \
ca-certificates \
wget
ENV GLIBC_RELEASE_VERSION 2.30-r0
RUN wget -nv -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub && \
wget -nv https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_RELEASE_VERSION}/glibc-${GLIBC_RELEASE_VERSION}.apk && \
apk add glibc-${GLIBC_RELEASE_VERSION}.apk && \
rm /etc/apk/keys/sgerrand.rsa.pub && \
rm glibc-${GLIBC_RELEASE_VERSION}.apk
Sorbet doesn't support Rails, but Tapioca can generate RBI files for it.
If you're not using Tapioca, you could use sorbet-rails, a community-maintained project which can help generate RBI files for certain Rails constructs.
Also see the Community page for more community-maintained projects!
Sord is a community-maintained project which can generate Sorbet RBI files from YARD docs.
Also see the Community page for more community-maintained projects!
The Sorbet team is actively involved in the Ruby 3 working group for static typing. There are some things we know and something we don't know about Ruby 3.
Ruby 3 plans to ship type annotations for the standard library. These type
annotations for the standard library will live in separate Ruby Signature (RBS)
files, with the *.rbs
extension. The exact syntax is not yet finalized. When
the syntax is finalized, Sorbet intends to ingest both RBS and RBI formats, so
that users can choose their favorite.
Ruby 3 has no plans to change Ruby's syntax. To have type annotations for methods live in the same place as the method definition, the only option will be to continue using Sorbet's method signatures. As such, the Sorbet docs will always use RBI syntax in examples, because the syntax is the same for signatures both within a Ruby file and in external RBI files.
Ruby 3 has no plans to ship a type checker for RBS annotations. Instead, Ruby 3 plans to ship a type profiler, which will attempt to guess signatures for code without signatures. The only way to get type checking will be to use third party tools, like Sorbet.
Ruby 3 plans to ship no specification for what the type annotations mean. Each
third party type checker and the Ruby 3 type profiler will be allowed to ascribe
their own meanings to individual type annotations. When there are ambiguities or
constructs that one tool doesn't understand, it should fall back to T.untyped
(or the equivalent in whatever RBS syntax decides to use for
this construct).
Ruby 3 plans to seed the initial type annotations for the standard library from Sorbet's extensive existing type annotations for the standard library. Sorbet already has great type annotations for the standard library in the form of RBI files which are used to type check millions of lines of production Ruby code every day.
From all of this, we have every reason to believe that users of Sorbet will have a smooth transition to Ruby 3:
- You will be able to either keep using Sorbet's RBI syntax or switch to using RBS syntax.
- The type definitions for the standard library will mean the same (because they will have come from Sorbet!) but have a different syntax.
- For inline type annotations with Ruby 3, you will have to continue using
Sorbet's
sig
syntax, no different from today.
For more information, watch this section from Matz's RubyConf 2019 keynote, which talks about his plans for typing in Ruby 3.
No. You can use an interface instead, or T.untyped
if you do
not control all of the code.
Duck typing (or, more formally, Structural typing) specifies types by their
structure. For example, Rack middleware accepts any object that has a call
method which takes one argument and returns a tuple representing an HTTP
response.
Sorbet does not support duck typing either for static analysis or runtime checking.
Fundamentally, Sorbet typechecks a single project at a time. Unlike other languages and compilers, it does not process files in a codebase one file at a time. This is largely due to the fact that Ruby does not have any sort of file-level import mechanism—in Ruby, code can usually be defined anywhere and used from anywhere else—and this places constraints on how Sorbet must be implemented.
Because Sorbet typechecks a single project at a time, it does not allow filtering the list of errors to a single file. Sometimes though, the number of errors Sorbet reports can be overwhelming, especially when adopting Sorbet in a new codebase. In these cases, the best way to proceed is as follows:
-
Put every file in the codebase at
# typed: false
(see Strictness Levels). Note that the default strictness level when there is no explicit# typed:
comment in a file is# typed: false
. The--typed=false
command line flag can be used to forcibly override every file's strictness level to# typed: false
, regardless of what's written in the file.Forcing every file to
# typed: false
will silence all but the most critical Sorbet errors throughout the project. -
Proceed to fix these errors. If there are still an overwhelming number of errors, tackle the errors that are reported earlier first. Sorbet's design means that errors discovered early in the early phases of typechecking can cause redundant errors in later phases.
-
Once there are no errors in any files at
# typed: false
, proceed to upgrade individual files to# typed: true
. Only new errors will be reported in these# typed: true
files, and not in any other files. Repeatedly upgrade as many individual files as is preferred. Note that many Sorbet codebases start off with all files at# typed: false
and gradually (usually organically) shrink the number of# typed: false
files over time.
If for some reason it's still imperative to limit the Sorbet error output to
only those errors coming from a single file and the steps above are not
acceptable, we recommend post processing the errors reported by Sorbet with
tools like grep
.
There are also third-party tools that offer the ability to sort and filter Sorbet's errors, like spoom.