For typing a Ruby billing library, how does a Sorbet sig block differ from an RBS signature or an inline # @rbs comment?
answer
- extend T::Sig, sig { params... }
- sig is Ruby code, runs at load
- sorbet-runtime checks calls
- srb tc for the static check
- # @rbs is only a comment
basics
~20 sA Sorbet sig is Ruby code, sig { params(cents: Integer).returns(Receipt) }, checked statically by srb tc and, with sorbet-runtime, on real calls. RBS lives in .rbs files or experimental # @rbs comments, has no runtime cost and is checked by Steep.
solid answer
~50 sSorbet signatures are **Ruby method calls**: the class does `extend T::Sig`, and `sig { params(cents: Integer, currency: Symbol).returns(T.nilable(Receipt)) }` sits above the `def`. Because it is code, the `sorbet-runtime` gem can wrap the method and raise when a real call breaks the signature, and `srb tc` runs Sorbet's static checker, whose strictness is set per file by a `# typed:` comment. RBS is a **separate language**: declarations in `sig/*.rbs`, or since RBS 4.0 experimental inline comments such as `# @rbs (Integer cents, ?currency: Symbol) -> Receipt?` above the `def`. Ruby ignores both forms at runtime, so there is no call overhead and no gem dependency; Steep checks them statically, and runtime checking is opt-in through `rbs/test/setup`. RBS signatures also ship inside gems' `sig/` directories and core RBS ships with Ruby, while Sorbet keeps third-party types in `.rbi` files.
code
ruby · 5 lines# Steepfile
target :lib do
signature "sig"
check "lib", inline: true # read # @rbs comments in lib/
endgo deeper
Recall that Sorbet writes sig blocks in Ruby while RBS uses its own syntax in .rbs files or # @rbs comments.
Explain which tool checks which: srb tc and sorbet-runtime for Sorbet, Steep for RBS, and why RBS has no runtime cost.
Compare the operational trade-offs: runtime enforcement versus zero footprint, .rbi versus sig/ and rbs collection, per-file typed sigils versus Steepfile targets, and the experimental status of inline RBS.
Frame the choice as an ecosystem bet: RBS is the notation Ruby ships and Sorbet now reads, while Sorbet's runtime checks buy enforcement at the cost of a dependency in every process.
## Two ways to write down Ruby types Ruby has two established type-annotation systems, and the difference an interviewer cares about is **where the types live and what runs them**. - **Sorbet** writes signatures in Ruby itself, using a DSL provided by its runtime library. - **RBS** is a separate type language, maintained with Ruby and bundled since Ruby 3.0, written in `.rbs` files or, experimentally, in comments. ## Sorbet: signatures as Ruby code ```ruby # typed: true class Billing::Charger extend T::Sig sig { params(cents: Integer, currency: Symbol).returns(T.nilable(Receipt)) } def charge(cents, currency: :usd) end end ``` - `extend T::Sig` adds the `sig` method to the class, and the block describes the next `def`. - `T.nilable(Receipt)` is Sorbet's spelling of "Receipt or nil". - The `# typed:` comment at the top of each file sets how strictly the static checker treats that file, which is how Sorbet is adopted file by file. - `srb tc` runs the **static** check over the project. - Because `sig` is executed when the class loads, the `sorbet-runtime` gem can also **check real calls** and raise when an argument or return value breaks the signature. That catches lies the static checker cannot see, at a small cost on every checked call. - Types for gems and for code Sorbet cannot see live in `.rbi` files, which use Ruby syntax. ## RBS: signatures as a separate language ```rbs # sig/billing/charger.rbs class Billing::Charger def charge: (Integer cents, ?currency: Symbol) -> Receipt? end ``` - Ruby never loads `.rbs` files, so signatures add **no runtime cost** and no runtime dependency. - **Steep** checks the code against them statically; editors and TypeProf read them too. - Gems can publish signatures in a `sig/` directory, the community keeps others in `gem_rbs_collection`, and signatures for core and the standard library come with the `rbs` gem. - Runtime checking exists but is opt-in: running tests with `RBS_TEST_TARGET='Billing::*'` and `-r rbs/test/setup` prepends checks onto the listed classes. ## Inline RBS: the same types next to the code The usual complaint about RBS, that signatures sit in another file and drift, is what **inline RBS** addresses. Introduced as **experimental** in RBS 4.0, with Steep 2.0 support through `check "lib", inline: true` in the Steepfile, it reads comments: ```ruby class Billing::Charger # @rbs (Integer cents, ?currency: Symbol) -> Receipt? def charge(cents, currency: :usd) end end ``` There is also a `#:` form and a doc style with one `# @rbs cents: Integer` line per parameter plus `# @rbs return: Receipt?`. An unannotated `def` is treated as `(?) -> untyped` unless a superclass method supplies a type. Limitations remain: no `class << self`, no top-level methods, no method visibility yet. ## Side by side | Aspect | Sorbet `sig` | RBS file / inline `# @rbs` | |---|---|---| | Written in | Ruby DSL (`T::Sig`) | RBS syntax, in `.rbs` or comments | | Static checker | `srb tc` | Steep | | Runtime checking | built in via `sorbet-runtime` | opt-in via `rbs/test/setup` | | Runtime dependency | yes | none | | Per-file strictness | `# typed:` comment | Steepfile targets and diagnostic presets | | Third-party types | `.rbi` files | gem `sig/` dirs, `rbs collection` | ## Moving between them `rbs prototype rbi` converts Sorbet RBI into RBS, and RBS 4.0 added `singleton(T)[S]` mainly for Sorbet integration, whose RBS support uses RBS's portable C parser. The systems are converging on RBS as the shared notation, but the checkers stay different tools. ## Choosing - If you want **runtime enforcement** as a safety net, Sorbet's runtime checks are the integrated option. - If you want **zero runtime footprint** and the notation Ruby itself ships, RBS with Steep fits, and inline `# @rbs` keeps it next to the code, accepting that the syntax is still experimental.
- Does an inline `# @rbs` comment change how the method behaves at runtime?No. It is an ordinary Ruby comment, so the interpreter ignores it and calls cost nothing extra. Only tools that parse it, such as Steep with `check ..., inline: true`, give it meaning. Runtime enforcement would need a separate mechanism such as RBS's test instrumentation.
- How can you check that hand-written RBS matches what the code really does, without Sorbet's runtime checks?Run the test suite with `RBS_TEST_TARGET='Billing::*' bundle exec ruby -r rbs/test/setup ...`. RBS prepends a module onto the listed classes that checks each call's arguments, block and return value against the signature and reports errors such as `ArgumentTypeError` or `ReturnTypeError`.
saying these in an interview costs you the question
- RBS signatures are enforced by the Ruby interpreter on every call.
- Sorbet signatures live in separate .rbs files.
- Inline # @rbs is a stable, finished feature.
- Steep reads Sorbet sig blocks directly.
- Using RBS forces a runtime gem dependency into production.