skip to content

How do you set up Steep to type-check a Ruby library against its RBS signatures, and what does steep check report?

level: middleimportance: should knowfreq 22%

answer

  1. steep init writes a Steepfile
  2. target: check "lib", signature "sig"
  3. rbs collection install for gems
  4. Ruby::NoMethod on a nilable receiver
  5. D::Ruby.default, lenient, strict

basics

~20 s

steep init writes a Steepfile; a target lists the Ruby to check and the sig directory. rbs collection install supplies gem signatures, and steep check reports mismatches such as Ruby::NoMethod on a value that may be nil.

solid answer

~40 s

Add `steep` to the Gemfile, run `steep init`, and edit the `Steepfile`: `target :lib do signature "sig"; check "lib" end`. Gem signatures come from **rbs collection**: `rbs collection init` writes `rbs_collection.yaml`, `rbs collection install` reads `Gemfile.lock`, downloads RBS into `.gem_rbs_collection/` and writes `rbs_collection.lock.yaml`, which you commit. Steep does **not** infer method types from Ruby; it checks each `def` against its RBS and infers only local variable and expression types. `steep check` prints diagnostics with IDs: `Ruby::NoMethod` when you call `.id` on a `Receipt?` without a nil check, `Ruby::ArgumentTypeMismatch`, `Ruby::ReturnTypeMismatch`, `Ruby::UndeclaredMethodDefinition`. Narrow with `if receipt`, `&.` or `case/when`. `configure_code_diagnostics(D::Ruby.strict)` or `.lenient` sets severities, and `--severity-level` decides what fails the run.

code

ruby · 8 lines
ruby
# Steepfile
D = Steep::Diagnostic

target :lib do
  signature "sig"
  check "lib"
  configure_code_diagnostics(D::Ruby.strict)
end

go deeper

for a junior

Recall the pieces: a Steepfile with a target, signatures in sig/, and steep check to run it.

for a middle

Explain that Steep checks code against declared RBS rather than inferring signatures, how rbs collection provides gem types, and how nil narrowing clears Ruby::NoMethod.

for a senior

Show operational control: diagnostic presets per target, --severity-level for CI, steep:ignore used sparingly, and rbs collection install as a CI step.

for a principal

Frame strictness as a dial: start lenient on legacy targets and strict on new ones, and measure progress with steep stats rather than by silencing diagnostics.

## What Steep is **Steep** is a static type checker for Ruby that uses **RBS** as its type language. You describe your API in `.rbs` files, and Steep reads your Ruby code and reports where the code and the signatures disagree. Its README states the division of labour directly: Steep does not infer types from Ruby programs; it requires declared types. It does infer the types of local variables and expressions inside a method, from the signatures of what they call. ## Setting it up 1. Add `gem "steep", require: false` to the Gemfile's development group and `bundle install`. 2. Run `steep init`, which writes a commented `Steepfile` template. 3. Declare a **target**, the unit Steep checks: ```ruby target :lib do signature "sig" check "lib" configure_code_diagnostics(D::Ruby.default) end ``` 4. Set up signatures for dependencies with **rbs collection**: `rbs collection init` creates `rbs_collection.yaml`, and `rbs collection install` resolves `Gemfile.lock`, copies each gem's RBS from the community `gem_rbs_collection` repository into `.gem_rbs_collection/`, and writes `rbs_collection.lock.yaml`. Commit the config and the lock file; ignore the directory. Gems that ship a `sig/` directory are loaded from the gem itself, and `library "monitor"` in the Steepfile adds standard-library signatures that the collection does not manage. 5. Run `steep check`. ## What `steep check` reports Each diagnostic has a file position, a severity and an ID: | Diagnostic | Typical cause in a billing library | |---|---| | `Ruby::NoMethod` | `charger.charge(500).id` when `charge` returns `Receipt?` | | `Ruby::ArgumentTypeMismatch` | passing `"500"` where the RBS says `Integer` | | `Ruby::ReturnTypeMismatch` | returning a String from a method declared `-> Integer` | | `Ruby::UndeclaredMethodDefinition` | a `def` with no RBS declaration | | `Ruby::UnknownConstant` | a gem class used before `rbs collection install` supplied its RBS | ## Nil and narrowing The most valuable report is usually `Ruby::NoMethod` on an **optional type**. RBS writes "Receipt or nil" as `Receipt?`, and Steep refuses a method call on it until the code proves it is not nil: - `if receipt` ... `end` narrows `receipt` to `Receipt` inside the branch; - `receipt&.id` is accepted and has type `Integer?`; - `case receipt when Receipt` narrows the same way; - `# @type var receipt: Receipt` is an annotation that tells Steep what you know, and should be rare. ## Controlling strictness - `configure_code_diagnostics` takes a preset: `D::Ruby.default` (applied if you set nothing), `D::Ruby.lenient`, `D::Ruby.strict`, `D::Ruby.silent`, `D::Ruby.all_error`, or a block that sets individual IDs. - The same diagnostic can differ by preset: `Ruby::NoMethod` is an error in `default` and `strict` but only information in `lenient`. - `steep check --severity-level=error` fails the run only on errors; the default threshold is `warning`. - `# steep:ignore NoMethod` on a line, or `steep:ignore:start` / `steep:ignore:end`, silences a specific spot. - `steep stats` prints the share of typed versus untyped method calls per file. ## Where people go wrong - Expecting Steep to invent signatures for undeclared methods. - Forgetting `rbs collection install` in CI, so every gem call becomes an unknown constant or method. - Scattering `untyped` or `@type var` annotations until the check passes and proves nothing.

  • Your CI's `steep check` reports unknown constants for every gem after a fresh checkout. What is missing?
    The gem signatures. `.gem_rbs_collection/` is not committed, so CI must run `rbs collection install`, which rebuilds it from the committed `rbs_collection.lock.yaml` and `Gemfile.lock`. Without that step Steep only sees core, standard-library and your own `sig/` declarations.
  • How would you type-check the test directory more loosely than lib?
    Add a second target in the Steepfile, for example `target :test do signature "sig/test"; check "test"; configure_code_diagnostics(D::Ruby.lenient) end`. Each target has its own sources, signatures and diagnostic preset, so production code can stay strict while tests are checked leniently.

saying these in an interview costs you the question

  • Steep infers method signatures from the Ruby code, so RBS files are optional.
  • Receipt? and Receipt are interchangeable to Steep.
  • .gem_rbs_collection/ should be committed instead of the lock file.
  • steep check fails on every hint and information diagnostic by default.
  • Steep runs the program to find type errors.