Nithin Bekal About

Adding Sorbet to a Rails project

14 Feb 2023

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.

Hi, I’m Nithin! This is my blog about programming. Ruby is my programming language of choice and the topic of most of my articles here, but I occasionally also write about Elixir, and sometimes about the books I read. You can use the atom feed if you wish to subscribe to this blog or follow me on Mastodon.