Steep Static Type Checking for Ruby Projects
Static type checker for Ruby
At a glance
- What is it?
- Steep adds gradual static type checking to Ruby through RBS signatures, configuration, and explicit annotations. It fits teams willing to maintain signatures, but it is not a drop-in type inference system.
- Who is it for?
- Ruby teams that already use or can gradually introduce RBS should evaluate Steep on a small `lib` directory and review its effect on signatures, gem type metadata, and existing `untyped` boundaries. Projects seeking automatic type inference or a checker that requires no separate declarations should not adopt it as-is.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Steep's Contract with Ruby Applications
Steep addresses a specific gap in Ruby's development workflow: Ruby programs can run without declared contracts for methods, parameters, return values, and instance variables, but static analysis needs those contracts. Steep does not inspect a Ruby program and infer its types. Instead, a project declares types in RBS files and writes annotations where dynamic Ruby features require clarification. This makes Steep suitable for maintainers who want type errors found before deployment while accepting a second description of application interfaces.
The intended users are Ruby developers working on codebases where gradual adoption matters. A project can target a directory such as `lib`, keep its signatures in `sig`, and focus checking on that boundary instead of requiring the entire repository to become fully typed at once. The README calls this gradual typing, and the trade-off is equally direct: useful checking depends on declarations that must be kept aligned with Ruby implementations.
This is not a checker for undeclared Ruby behavior. A method can be visible at runtime and still have no useful signature. Steep reports contract mismatches, including declarations for methods that Ruby does not implement, but that only helps when the RBS describes the intended behavior accurately. The signature files therefore become maintained source artifacts rather than optional documentation.
How Steep Uses RBS, Steepfile, and Annotations
A typical data flow begins with `Steepfile`, which maps checked Ruby code to its signatures. Type information for gems recorded in `Gemfile.lock` is loaded through `rbs collection`. Steep then combines those signatures, declarations under the configured signature directory, and local annotations before checking source code.
RBS describes classes, instance variables, public and private methods, singleton methods, generic types, union types, and nullable return values. For example, `Array[Email | Phone]` represents a collection containing either email or phone objects. A method declared as returning `(String | nil)` includes Ruby's `nil`, while an `untyped` method parameter disables type checking inside the affected method. That escape point is useful during incremental adoption, but it can also remove checks without an obvious warning to the reviewer.
Steep supports narrowing through a `case` expression when the value has a declared union type. Its Phone example checks `when Phone`, allowing `contact.country` inside that branch. Dynamically generated readers need `@dynamic` annotations because Steep otherwise does not treat methods declared by `attr_reader` as ordinary `def` definitions. The checker's effectiveness consequently depends on how closely the signatures reflect how the Ruby code generates methods.
Installing Steep and Checking the First Directory
The README documents RubyGems installation and requires Ruby 2.6 or later. Install the published `steep` gem in the environment where the project should run its checks:
gem install steepAfter RubyGems completes, generate a `Steepfile` from the project directory:
steep initThe command is documented to generate `Steepfile`. The initial file is small, and each block connects a Ruby directory with the directory holding its RBS declarations:
target :app do
check "lib"
signature "sig"
endThe exact `target` name and directory names are project choices. In this example, `lib` is checked against signatures in `sig`. Run the checker after declarations and annotations have been added:
steep checkA first successful or diagnostic run depends on those files. The README's example ends with a `MethodDefinitionMissing` report because the RBS declares `Phone.countries` but the Ruby class does not implement it. That output is the expected shape of a contract mismatch, not an installation log. If checking does not start with the intended directories, review `Steepfile` before changing Ruby code.
Writing the Example Person, Email, and Phone Contracts
The README's example begins with separate `.rbs` declarations for three classes. This is where Steep departs most sharply from automatic Ruby type inference. The developer states the return types of readers and helpers, the types of instance variables, and the type of `guess_country`. Constructors in the example return `untyped`, reflecting the signature's explicit declaration rather than a requirement inferred from Ruby syntax.
The Ruby implementation then supplies the operational details. `Person` initializes its name and contact list, while `Email` and `Phone` expose attributes and equality behavior. The signature union `Email | Phone` gives `guess_country` a finite choice for its `case` expression. `compact.first` is compatible with a nullable country result, while direct calls to `contact.country` outside a proven `Phone` branch would not have the same declared type information.
Private methods and instance variables must also be declared when relevant. That breadth matters during migration. Declaring only public interfaces may leave implementation details unchecked or unavailable to Steep's analysis. Conversely, broad signatures can make the checker less informative without making the application invalid.
RBS Collections, Prototype Generation, and Missing Dependency Types
Steep's documentation directs projects to initialize and install an RBS collection rather than treating every dependency signature as an unrelated local file:
rbs collection init
rbs collection installAfter those commands, RBS files for gems in `Gemfile.lock` are loaded via `rbs collection`. A `library` call is intended only for an RBS that the collection does not manage. This setup adds a dependency-signature dimension to adoption. A successful local implementation does not by itself establish that every gem has the signature quality or availability required for useful checking.
For an existing Ruby file, `rbs prototype rb` can produce a starting declaration:
rbs prototype rb lib/person.rb lib/email.rb lib/phone.rbThe output contains classes, methods, instance variables, and constants, but its types are deliberately crude. The README specifically notes an unusual inferred constructor result for `Person` and the absence of methods extracted from `attr_reader`. It therefore describes prototype output as a starting point, not a finished contract. Steep also supports `rbi` for Sorbet RBI input and `runtime` for runtime API input. Neither documented mode changes the core requirement that developers review and refine the resulting declarations.
Steep's Limits Compared with Untyped Ruby Execution
The clearest alternative is running the Ruby application without static type checking. Plain Ruby executes behavior dynamically and does not require a separate RBS declaration for every method. That approach avoids the signature maintenance and incomplete declaration risks inherent to Steep. It is the wrong choice for a project that needs contract checking during editing or in CI, but it requires fewer typing artifacts.
Steep's `untyped` escape hatch creates a middle path. The README shows `other` in `Email#==` as `untyped`, with no type errors detected in that method. This is useful when adopting Steep around existing dynamic code, yet it means that a green `steep check` result does not mean every method was checked. The Phone example goes further and adds `@type var other: Phone` inside an `is_a?` condition because a type case cannot narrow an `untyped` value.
Other limitations are concrete. Steep does not infer types from Ruby programs. Methods defined without `def` syntax require `@dynamic` declarations. Type narrowing depends on declared unions and appropriate narrowing expressions. The README does not document automatic rollback, signature migration tooling, or a policy for incomplete dependency signatures. Editor support also varies: Steep implements some Language Server Protocol features, while the README points VS Code users to a separate plugin and lists specific integrations for Sublime Text, Vim, and Neovim.
Steep Releases, Rake Integration, and Upgrade Scope
The repository is not archived, and its last push was on 2026-10-13. The listed releases include `v2.1.0` on 2026-08-27, `v1.10.0` on 2025-03-18, and the prerelease `v1.10.0.pre.3` on 2025-03-14. These dates establish recent repository activity and a recent stable release. They do not establish compatibility with every Ruby or gem version, because the README only states that Steep requires Ruby 2.6 or later and does not provide an upgrade matrix.
Teams can integrate checking through configurable Rake tasks. The documented configuration sets an error severity level and enables verbose watch output:
require "steep/rake_task"
Steep::RakeTask.new do |t|
t.check.severity_level = :error
t.watch.verbose
end
task default: [:steep]The README says to use `bundle exec rake -T` to inspect available tasks. For contributors working from a checkout, it documents `bin/setup` for dependencies, `rake test` for tests, `bin/console` for an interactive prompt, and `bundle exec rake install` for installing the local gem. The release process instead uses the `Release gem` workflow, as stated in `doc/release.md`.
Steep is distributed under the MIT license. An upgrade can affect the checker version, Ruby requirement boundaries, RBS handling, and integration behavior, although the README excerpt does not enumerate a compatibility matrix. License acceptance does not remove that upgrade review, especially for projects using Steep inside Bundler, Rake, or CI.
Editorial conclusion
Ruby teams that already use or can gradually introduce RBS should evaluate Steep on a small `lib` directory and review its effect on signatures, gem type metadata, and existing `untyped` boundaries. Projects seeking automatic type inference or a checker that requires no separate declarations should not adopt it as-is. The first project-specific check is whether the intended code and dependencies can be represented with useful RBS signatures before running `steep check` across the application.
Frequently asked questions
How do I install Steep for Ruby?
The README documents `gem install steep`. Steep requires Ruby 2.6 or later.
Does Steep infer types from Ruby source code?
No. Steep requires developers to declare types in RBS files and add annotations for relevant dynamic code. `rbs prototype` can generate a rough starting declaration, but its output still requires review.
How do I configure Steep to check a Ruby project?
Run `steep init` to generate a `Steepfile`, then map checked source such as `lib` to signatures such as `sig`. Dependency signatures are handled through the documented `rbs collection` setup.
What does `untyped` mean in Steep?
An `untyped` value causes type checking to be skipped for the affected method or expression. The README explains that an `other` parameter typed as `untyped` allows no type errors to be detected inside that method.
Can Steep check methods created by `attr_reader`?
The README uses a `@dynamic name, contacts` annotation for readers defined through `attr_reader`. Steep can skip checking when the methods are implemented that way unless their definitions are represented appropriately.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/soutaro-steep)Was this useful?