# RBS: the type signature language Ruby decided to build instead of infer

> A signature language that describes the shape of a Ruby program without changing how the program runs, shipped under the ruby organisation, with a prototype command that turns existing code into a starting signature. Recent versions tightened the Ruby floor and started accepting non-ASCII identifiers.

**ruby/rbs** — The type signature language for Ruby

- Repository: https://github.com/ruby/rbs
- Stars: 2,183 · Forks: 258
- Language: Ruby
- License: NOASSERTION
- Published: 2026-10-08 · Updated: 2026-10-08 · Language: en
- Canonical page: https://hysenlabs.com/projects/ruby-rbs

## What the language actually describes

RBS is a language to describe the structure of Ruby programs. Written down in an RBS file, you get the definition of a class or module: the methods defined in it, the instance variables and their types, and the inheritance and mix-in relations. It also allows declaring constants and global variables.

The example in the README is a chat application, and it packs a lot of syntax into thirty lines because it is the clearest available statement of what the language covers:

```rbs
module ChatApp
  VERSION: String

  class User
    attr_reader login: String
    attr_reader email: String

    def initialize: (login: String, email: String) -> void
  end

  class Message
    attr_reader id: String
    attr_reader string: String
    attr_reader from: User | Bot                     # `|` means union types: `#from` can be `User` or `Bot`
    attr_reader reply_to: Message?                   # `?` means optional type: `#reply_to` can be `nil`

    def initialize: (from: User | Bot, string: String) -> void

    def reply: (from: User | Bot, string: String) -> Message
  end

  class Channel
    attr_reader name: String
    attr_reader messages: Array[Message]
    attr_reader users: Array[User]
    attr_reader bots: Array[Bot]

    attr_reader each_member: () { (User | Bot) -> void } -> void  # `{` and `}` means block.
                   | () -> Enumerator[User | Bot, void]   # Method can be overloaded.
  end
end
```

Four things to notice. Types are written after a colon, which reads differently from most languages. `|` forms a union. A trailing `?` makes the type optional, meaning nil is permitted. And methods can be overloaded, as `each_member` is, with a block form and an enumerator form separated by `|`.

What is absent is equally important. Nothing in an RBS file executes, nothing changes how Ruby loads your class, and there is no annotation syntax requiring you to edit method bodies. The signatures live alongside the code, which is what makes the approach tolerable for a language that has spent twenty years without static types.

## Two version targets, and they are not the same number

The README states two version targets, and conflating them leads to the wrong conclusion about what you need installed.

The standard library signatures target the latest release of Ruby, listed as `4.0` as of 2026. The library code targets non-EOL versions of Ruby, listed as `>= 3.3` as of 2026. So the `stdlib/` directory in the tree describes a newer Ruby than the tool itself is guaranteed to run on.

That split exists because the signatures describe the standard library as it will be, while the code has to keep running on every supported interpreter. It also means a signature that references something absent on your Ruby will not help you, and that the value of the `stdlib/` directory depends on which Ruby you are actually running.

The 4.2 release tightened this. Its notes say that Ruby 3.2 reached EOL on 2026-04-01 and that the release requires Ruby 3.3 or later, with applications on 3.2 staying on the 4.1 line. Combined with the repository's 214 open issues and a last push on 2026-09-28, the picture is of a project in active maintenance with a deliberate policy of dropping interpreter support on a schedule tied to Ruby's own.

Worth flagging as a metadata inconsistency: the repository's license field reads NOASSERTION, but the tree root contains both `BSDL` and `COPYING`. The actual terms are in those files. The unset field is a publishing oversight rather than an absence of a license, and anyone shipping RBS in a compliance-reviewed environment should read `BSDL` directly.

## rbs prototype is the command that matters for existing code

The gem ships an `rbs` command line tool, described as something to demonstrate what RBS can do and to help develop RBS itself. It exposes `rbs version`, `rbs list`, `rbs ancestors ::Object`, `rbs methods ::Object` and `rbs method Object then`. Useful for inspection.

The command an end user reaches for is `rbs prototype`, which generates boilerplate signature declarations from Ruby files. Given a plain class:

```ruby
# person.rb
class Person
  attr_reader :name
  attr_reader :contacts

  def initialize(name:)
    @name = name
    @contacts = []
  end

  def speak
    "I'm #{@name} and I love Ruby!"
  end
end
```

Running the prototype produces:

```console
$ rbs prototype rb person.rb
class Person
  @name: untyped

  @contacts: untyped

  attr_reader name: untyped

  attr_reader contacts: untyped

  def initialize: (name: untyped) -> void

  def speak: () -> ::String
end
```

Read that output as a starting point and not an answer. Every field is `untyped` because the generator infers shapes, not intent, and the README says plainly that you should edit the output to match your signature more accurately. One useful detail did survive inference: `speak` came back as `::String` rather than `untyped`, since a bare interpolated string literal has an obvious type.

There are three modes. `rb` generates from available Ruby code, `rbi` generates from Sorbet RBI, and `runtime` generates from the runtime API, which is deprecated and tracked in issue 2841. If you are coming from Sorbet, `rbi` is the migration path. If you are starting fresh, `rb` is the only mode with a future.

## Environment and definition, the two library concepts

Using RBS as a library means understanding two concepts that the CLI abstracts away.

An _environment_ is a dictionary tracking all declarations. The question it answers is: what is associated with the `String` class? A _definition_ gives the detail of a class. The question it answers is: what is the return type of `gsub`?

The README shows both in about twenty lines, loading an environment, building a definition, and asking it about a real method:

```rb
require "rbs"

loader = RBS::EnvironmentLoader.new()

# loader.add(path: Pathname("sig"))   # Load .rbs files from `sig` directory
# loader.add(library: "logger")       # Load logger library

environment = RBS::Environment.from_loader(loader).resolve_type_names

# ::String
string = RBS::TypeName.new(name: :String, namespace: RBS::Namespace.root)

# Class declaration for ::String
decl = environment.class_decls[string]

# Builder provides the translation from `declaration` to `definition`
builder = RBS::DefinitionBuilder.new(env: environment)

# Definition of instance of String
instance = builder.build_instance(string)

puts instance.methods[:gsub].method_types.join("\n")
# Outputs =>
#  (::Regexp | ::string pattern, ::string replacement) -> ::String
#  (::Regexp | ::string pattern, ::Hash[::String, ::String] hash) -> ::String
#  (::Regexp | ::string pattern) { (::String match) -> ::_ToS } -> ::String
#  (::Regexp | ::string pattern) -> ::Enumerator[::String, self]
```

Two details in that output are worth pausing on. The four overloads include a block form, and the block's return type is the internal `_ToS` interface, not `String`, because Ruby's `gsub` accepts anything convertible to a string. And the last overload returns an `Enumerator` whose type argument is `self`, a detail that is why the signature language needs `self` as a distinct type. Instance definitions and singleton definitions are built separately, and `String.gsub` does not exist, so the same query against `build_singleton` returns nothing.

This is the layer a type checker written on top of RBS consumes. If you are using a checker rather than writing one, this is where its behaviour is determined.

## Non-ASCII identifiers arrived, and argument forwarding did not

RBS 4.2 accepts non-ASCII characters in identifiers. Method names, instance variable names and parameter names can be written in any script, and other names may contain non-ASCII characters as long as they start with an ASCII letter, which is what distinguishes a constant from a method name. The release notes illustrate it with a `Greeter` class whose instance variable is written in Japanese and whose method is named with a Japanese greeting.

The mechanism behind that rule is a practical one. Ruby itself decides what a constant is by looking at the first character, so RBS has to use the same test or it would disagree with the language it describes.

The other headline feature in the same release is a story about staged delivery. The parser and the AST support Ruby-style `...` forwarding parameters in method types, but the layers built on top of the parser do not yet. The syntax is disabled by default in 4.2, is not enabled by the public `RBS::Parser` API, and therefore cannot be used in regular signatures yet. It is present in the grammar in advance of the pipeline that would consume it.

The rest of 4.2 is the unglamorous half that actually matters to users of the standard library signatures. Updated classes and modules include `ERB`, `ERB::DefMethod`, `IO`, `Monitor`, `MonitorMixin::ConditionVariable`, `Singleton`, `StringIO`, `Zlib::GzipFile` and `Zlib::GzipWriter`, with deterministic `untyped` return types being replaced by concrete ones. A signature improvement like that does not show up in a changelog headline, but replacing a placeholder type with a real one is what turns a checker from complaining about everything into complaining about a few things. The pre-release 4.2.1.pre.1 followed with cleanup of obsolete Ruby version guards and a change to make `rbs collection` say where the gem list comes from.

## Signatures for gems that do not ship their own

The stated target version is the latest release of Ruby, so someone has to write and maintain the standard library signatures, and that is structured work rather than volunteer chaos. The tree separates the concerns clearly: `core/` for core classes, `stdlib/` for standard library, `sig/` for the gem's own signatures, `lib/` for the Ruby implementation, `src/` and `include/` for the parts that are not Ruby at all.

That last point is the one that surprises people. RBS ships a C extension, and the repository also contains `rust/`, `ext/`, `wasm/`, `include/`, a `Dockerfile.jruby`, `.clang-format` and `.clangd`. There is a `Steepfile` at the root, meaning the project dogfoods Steep, the type checker built on RBS. A `schema/` directory and `templates/` suggest generated or validated content, and `benchmark/` alongside `goodcheck.yml` and `.rubocop.yml` shows the usual linting stack. Contributing guides are split by audience: architecture, core and standard library signature contribution, writing signatures, stdlib signatures, syntax, RBS by example, collection, using `Data` and `Struct`, and releasing a gem with RBS.

Formatting the C sources is a documented two-step affair with `rake format:c` to rewrite and `rake format:c_check` to verify, requiring clang-format installed through brew, apt or choco depending on platform. For development after checkout, `bin/setup` installs dependencies, `bundle exec rake test` runs the tests, and `bin/console` gives an interactive prompt. Releases are cut by a Release gems workflow rather than from a working copy, per `docs/release.md`, which is a sensible guard against accidental publishes.

The gap RBS cannot fill on its own is third-party gems. `gem_rbs_collection` exists for the case where a dependency does not ship RBS files, maintained alongside the main repository with a `types` channel in the ruby-jp Slack workspace and an `rbs` channel in the Ruby Discord server. Check there before concluding a gem is untyped.

## Conclusion

RBS is a reasonable fit when you want types on Ruby without changing runtime behaviour and without adopting a type checker that has to infer everything from scratch. `rbs prototype rb` is the practical starting point for existing code, and the expectation is that you edit its `untyped` output by hand, because a generated signature documents shape rather than meaning. Two practical notes before you commit. The `runtime` prototype mode is deprecated and tracked for removal, so new tooling should target `rb`, with `rbi` there when you are migrating from Sorbet. And 4.2 raises the floor to Ruby 3.3 because Ruby 3.2 reached end of life on 2026-04-01, which means applications still on 3.2 stay on the 4.1 line rather than tracking master. The repository reports its license field as unset even though `BSDL` and `COPYING` sit in the tree root, so check those two files for the actual terms rather than relying on the metadata. Third-party gems that ship no signatures of their own are covered by the separate `gem_rbs_collection` repository, which is the piece to look at before assuming a dependency is untyped.

## FAQ

### What does RBS stand for?

In the Ruby context it is the type signature language for Ruby, hosted at `ruby/rbs` and installed as the `rbs` gem. It is a language for describing the structure of Ruby programs: class and module definitions, methods, instance variables and their types, inheritance and mix-in relations, plus constants and global variables. Signatures live in separate `.rbs` files and do not change runtime behaviour.

### How do I generate RBS signatures for code I already have?

Use `rbs prototype rb path/to/file.rb`. It emits every class, method, instance variable and constant it finds, with return and parameter types as `untyped` where it cannot infer them. Treat the output as a draft and edit it: the command is described as a starting point rather than an accurate description of your intent. The `runtime` mode is deprecated, and `rbi` is the mode to use when migrating from Sorbet.

### What Ruby version does RBS 4.2 require?

Ruby 3.3 or later. The 4.2 release notes state that Ruby 3.2 reached end of life on 2026-04-01, and that applications still on 3.2 should stay on the 4.1 line. Separately, the library code targets non-EOL Ruby versions while the standard library signatures target the latest release, which is listed as Ruby 4.0 as of 2026.

### My dependency has no RBS files. Where are its types?

Look in `gem_rbs_collection`, the repository of third-party RBS type definitions maintained for the case where a dependency does not ship RBS files itself. It sits alongside `ruby/rbs` and is the right first stop before concluding a gem is untyped. Once you have the signatures, point the loader at them or add the library with `loader.add(library: "logger")` in the equivalent call.

## Sources

- [Issues](https://github.com/ruby/rbs/issues)
- [README](https://github.com/ruby/rbs/blob/master/README.md)
- [Releases](https://github.com/ruby/rbs/releases)
- [ruby/rbs on GitHub](https://github.com/ruby/rbs)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ruby-rbs
