Type Signatures: RBS and Sorbet
Prerequisites: Classes and objects, Modules and mixins
Ruby is dynamically typed: a mistake like passing a string where a number is expected is only found when that line runs. In large codebases, optional type checking catches these mistakes before the code runs, and makes editors' autocomplete much smarter.
Ruby has two main approaches. Neither changes how your code runs — they're checked by a separate tool.
RBS — the official signature language
RBS signatures live in separate .rbs files, next to your Ruby code:
# lib/cart.rb
class Cart
def initialize = @items = []
def add(name, price)
@items << { name:, price: }
self
end
def total = @items.sum { |item| item[:price] }
end
# sig/cart.rbs
class Cart
@items: Array[{ name: String, price: Integer }]
def initialize: () -> void
def add: (String name, Integer price) -> self
def total: () -> Integer
end
A checker such as Steep then reports calls that don't match:
$ bundle exec steep check
app.rb:3:9: [error] Cannot pass a value of type `::String` as an argument of type `::Integer`
│ cart.add("pen", "3")
rbs prototype rb lib/cart.rb generates a first draft of signatures
for you to refine. RBS ships with Ruby, and the standard library
already has signatures.
Sorbet — inline annotations
Sorbet, created at Stripe, writes signatures in the Ruby file itself, and can also check them at runtime:
# typed: true
require "sorbet-runtime"
class Cart
extend T::Sig
sig { params(name: String, price: Integer).returns(T.self_type) }
def add(name, price)
# ...
self
end
end
Should you use them?
- For scripts and small apps, clear names, tests, and a good editor are usually enough.
- For large, long-lived codebases with many contributors, gradual typing pays for itself — you can start with the most important classes and expand over time.
- Use whichever the project already uses; Sorbet is common in large companies' Rails codebases, RBS in libraries and newer projects.