Adding Sorbet to a Rails project
Sorbet is a static type checker for Ruby. In this post, we will walk though how we can add sorbet to a new Rails app. Along the way, we will also see a few examples of how sorbet can help you identify and fix bugs in your code.
Setting up
Let’s start off by creating a simple Rails project and setting up the database.
rails new blog
cd blog
rails db:setup
The sorbet team recommends tapioca for setting up sorbet in your projects. Let’s add tapioca to the gemfile. This will also install sorbet as a dependency.
group :development do
gem 'tapioca', require: false
end
Now, let’s run the tapioca init command to set up our project for typecheking. This will add the sorbet configuration and fetch type annotations for the gems in your application.
tapioca init
Rails dynamically generates
a lot of methods and constants.
We can use the newly created bin/tapioca
binstub
to generate type definitions for these methods.
bin/tapioca dsl
This will add .rbi
files
to sorbet/rbi/dsl
.
Now, we’re ready to run the typechecker:
srb typecheck
As of this writing,
running the type checker
with a Rails 7 application causes type errors
related to Turbo::Streams::ActionHelper
.
This is because of how Rails performs autoloading
(more details here)
and should hopefully be fixed in the next version.
For now, you can get around this
by creating sorbet/rbi/shims/turbo-rails.rbi
with:
# typed: true
module ::Turbo::Streams::ActionHelper
end
After this,
running srb typecheck
should show no errors.
Adding a model
So far,
we’ve only type checked
an empty Rails application,
which isn’t very exciting.
Let’s start adding some code
so we can see how sorbet can help us.
First, let’s generate a Post
model:
rails generate model Post title body:text
rails db:migrate
Add # typed: true
to the top of the Post
model.
This will tell sorbet to statically type check this file.
Next, we will run the tapioca dsl
command again
to generate a file with
type annotations for the Post
model.
bin/tapioca dsl
This will create sorbet/rbi/dsl/post.rbi
and generate type signatures
for the methods generated by Rails,
such as title
or body
.
If you’re unfamiliar with how to write sorbet type signatures,
this file will give you some examples.
Let’s cause the typechecker to fail
by adding a method and deliberately making a typo.
Here I’ve mis-typed title
and titel
:
class Post
def id_and_title
[id, titel].join(" ")
end
end
Now running srb typecheck
will show the following error:
app/models/post.rb:6: Method titel does not exist on Post https://srb.help/7003
6 | [id, titel]
^^^^^
Did you mean title? Use `-a` to autocorrect
app/models/post.rb:6: Replace with title
6 | [id, titel]
^^^^^
sorbet/rbi/dsl/post.rbi:545: Defined here
545 | def title; end
^^^^^^^^^
Errors: 1
Not only does sorbet tell you
that the titel
method doesn’t exist,
it has figured out
that you probably meant to type title
.
It also helpfully offers to automatically fix this
if you run the command again with the -a
flag.
srb typecheck -a
And voila! Sorbet has edited the file and fixed the method name!
Adding a type signature
Next, let’s try adding a type signature to the method:
sig { returns(String) }
def id_and_title
[id, title].join(" ")
end
This time,
sorbet tells us that sig
and returns
aren’t defined.
This is because we need to add extend T::Sig
to the model
to be able to define type signatures.
Once again, we can run srb typecheck -a
to autocorrect this.
Now let’s try breaking this signature by removing the call to join, so the method now returns an array:
def id_and_title
[id, title]
end
Now srb typecheck
will show us:
app/models/post.rb:9: Expected String but found [T.nilable(Integer), T.nilable(String)] for method result type https://srb.help/7005
9 | [id, title]
^^^^^^^^^^^
Expected String for result type of method id_and_title:
app/models/post.rb:8:
8 | def id_and_title
^^^^^^^^^^^^^^^^
Got [T.nilable(Integer), T.nilable(String)] (2-tuple) originating from:
app/models/post.rb:9:
9 | [id, title]
^^^^^^^^^^^
Errors: 1
Unfortunately, this time sorbet doesn’t how to fix this, but the error message shows us that we expect a string, but getting an array of two items, and this should help us figure out how to fix it.
Wrapping up
Now that you have sorbet set up on your app, you can choose how much you want to integrate it into your application. Sorbet allows gradual typing, which means you can enable typechecking for some of your files, while ignoring others.
Aside from typechecking, sorbet also provides an LSP, which allows editors and IDEs to provide features like code completion, go-to-definition, and showing errors right in the editor.
As we’ve seen above, you can gain many the benefits of sorbet, before writing even a single type signature. I hope this article inspires you to adopt sorbet in your projects! :)
Here are a few resources and articles about sorbet.