Type Checking with RBS, TypeProf, Steep and Sorbet
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18). Ruby does not check types before a program runs and has no type annotations in its syntax. It is still possible to describe the types of a program, in a separate language called RBS, and to check code against those descriptions with tools. This page explains that language and the tools around it: RBS and TypeProf, which ship with Ruby as bundled gems, and Steep and Sorbet, which are community projects that you install yourself and that are not part of Ruby. The aim is to help you decide how much of this machinery a project needs, because the language itself never requires it.
It builds on Methods and Parameters (the parameter kinds that method types describe), Struct, Data and Value Objects (how value objects appear in signatures), RubyGems and Bundler (bundled gems and Gemfile.lock, which rbs collection reads) and Testing with Minitest, whose tests double as the workload for runtime signature checking below. The tools that look at style rather than types are on Style, Linting and Static Analysis.
The examples use the shelf gem (a small library catalogue, see the shelf gem source). The RBS files were validated with rbs validate, the Ruby files were syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop 1.92 using the gem’s configuration, and the outputs shown are real. TypeProf 0.31.1 and RBS 3.10.0, the versions bundled with Ruby 4.0, produced the RBS and TypeProf output. Steep, Sorbet and rbs-inline are not installed on the machine used for this page, so their listings are described from their official documentation and were not run; each says so.
The map of the tools
There are two families. RBS keeps types in files next to the code, so the Ruby stays plain and the types can be added to a gem that you do not control. Sorbet keeps types in the code, as sig blocks that run, and uses its own RBI files for the rest. Both are gradual: anything not described is untyped and is accepted.
| Tool | Role | Ships with Ruby 4.0? |
|---|---|---|
RBS |
Signature language and the |
Yes, bundled gem 3.10.0 |
TypeProf |
Infers types from code, language-server mode |
Yes, bundled gem 0.31.1 (its README calls it experimental) |
rbs-inline |
Writes RBS as comments in the Ruby file |
No (community) |
Steep |
Static checker for RBS (community, Soutaro Matsumoto) |
No |
Sorbet |
Static checker plus runtime |
No |
The bundled versions are listed in the release notes of each Ruby, and they were confirmed by listing the gems of each installed Ruby.
| Ruby | RBS | TypeProf |
|---|---|---|
3.1.7 |
2.7.0 |
0.21.3 |
3.2.11 |
2.8.2 |
0.21.3 |
3.3.12 |
3.4.0 |
0.21.9 |
3.4.11 |
3.8.0 |
0.30.1 |
4.0.7 |
3.10.0 |
0.31.1 |
Bundled gems are installed with Ruby but are not loaded by require under Bundler unless they are in the Gemfile. A project that runs rbs or typeprof in CI should list them there, with versions.
group :development do
gem "rbs", "~> 3.10"
gem "typeprof", "~> 0.31"
end
What RBS is
RBS is a language with its own parser, a file extension (.rbs) and a convention of keeping files in sig/. A signature file never runs. It declares classes, modules, methods, constants and types, and tools read it. It describes the shape of an API, not its implementation, so the same file can document a gem, drive a checker, and generate documentation or editor completion.
The listing below is the signature for the shelf gem. It shows classes, a module with an interface, an alias for a union of symbols, overloads, a generic mixin and a block type.
module Shelf
VERSION: String
class Error < StandardError
end
class Isbn
attr_reader digits: String
def self.new: (String digits) -> Isbn
| (digits: String) -> Isbn
def initialize: (digits: String) -> void
def to_s: () -> String
private
def valid?: (String value) -> bool
end
class Author
attr_reader name: String
def self.new: (String name) -> Author
| (name: String) -> Author
end
class Book
attr_reader isbn: Isbn
attr_reader title: String
attr_reader author: Author
def self.new: (Isbn isbn, String title, Author author) -> Book
| (isbn: Isbn, title: String, author: Author) -> Book
def to_s: () -> String
end
type loan_state = :available | :lent | :overdue | :returned
class Loan
STATES: Array[loan_state]
attr_reader book: Book
attr_reader state: loan_state
def initialize: (Book book, ?state: loan_state) -> void
def describe: () -> String
end
module Store
interface _Store
def all: () -> Array[Book]
def add: (Book book) -> self
end
class Memory
include _Store
def initialize: () -> void
end
end
class Catalog
include Enumerable[Book]
def initialize: (?store: Store::_Store) -> void
def add: (Book book) -> self
def each: () { (Book) -> void } -> void
| () -> Enumerator[Book, void]
end
class Config < Struct[untyped]
attr_accessor store: Store::_Store
attr_accessor loan_days: Integer
end
self.@config: Config?
def self.config: () -> Config
def self.configure: () { (Config) -> void } -> Config
end
Source: RBS repository: docs/syntax.md
Points worth reading closely in this file:
-
Isbn,AuthorandBookare created withData.define, which definesnewwith both positional and keyword forms, sonewhas two overloads separated by|.rbs prototype runtimegenerates the same two overloads, and the RBS documentation onDataandStructshows the same shape. Theinitializesignature describes the keyword form thatIsbnoverrides. -
type loan_state = :available | :lent | :overdue | :returnedis a type alias. Aliases start with a lowercase letter, classes and modules with an uppercase one. -
interface _Storedeclares only what a collaborator must be able to do. An interface is not a class: nothing inherits from it, andStore::Memoryconforms by listinginclude _Store, and any other object conforms by having matching methods. This is duck typing written down. -
include Enumerable[Book]instantiates the generic moduleEnumerablewith an element type. -
eachhas two overloads, with and without a block. The block type() { (Book) -> void } -> voidreads as "takes a block that receives a `Book`". -
selfas a return type means "the receiver’s own type", which keeps chains such ascatalog.add(a).add(b)typed even in subclasses. -
self.@config: Config?declares a class-level instance variable that may benilbefore it is memoized.
The type syntax in one tour
The next file is a tour of the rest of the type syntax. It is not part of the gem and exists so that every construct is validated once.
# A tour of the RBS type syntax.
type isbn_like = String | Shelf::Isbn
type options = { title: String, ?year: Integer }
type callback = ^(String) -> void
interface _Walk[T]
def each: () { (T) -> void } -> void
end
module Greeter : _Walk[String]
def greet_all: () -> Array[String]
end
class Box[T < Comparable]
attr_reader value: T
attr_accessor label: String?
attr_writer secret: Integer
SIZE: Integer
def initialize: (T value, ?label: String?) -> void
def self.of: [U < Comparable] (U) -> Box[U]
def max: (Box[T] other) -> Box[T]
def pair: () -> [T, String]
def fetch: (Symbol key, ?String default) -> (String | Integer)
def lookup: (*String names, **Integer limits) ?{ (String) -> void } -> void
def kind: () -> (:small | :large)
def me: () -> self
def new_like: () -> instance
def to_proc: () -> ^(T) -> T
alias same max
private
def internal: () -> untyped
end
class Pair[out A, in B]
end
Source: RBS repository: docs/syntax.md
| Syntax | Meaning |
|---|---|
|
A union: either type. |
|
Shorthand for |
|
An intersection: the object must be both. |
|
A tuple of fixed length with a type per position. |
|
A record type for hash literals with symbol keys; |
|
Literal types: a symbol, string, integer or boolean value used as a type. |
|
A proc type, written with a caret. |
|
The escape hatch: compatible with everything in both directions, and the way gradual typing is expressed. |
|
The supertype of all values and the type of no value (for example a method that always raises). |
|
|
|
The result is not used; callers must not depend on it. |
|
The |
|
The type of an instance of the enclosing class, of its singleton class, and of the receiver. |
|
A rest parameter, keyword rest parameter and optional block. |
|
An optional keyword parameter that also accepts |
|
A type parameter with an upper bound; |
Four rules explain most errors that beginners meet:
-
Interface names start with an underscore (
_Store), classes and modules do not, and aliases are lowercase. The parser enforces this. -
A method type always spells out the parameters and the result:
() -> Stringtakes nothing, and there is no implicit return type. -
Anything private is introduced with
privateon a line of its own, as in Ruby, and applies to the declarations that follow. -
Every type used must exist: a name that is not declared in any loaded directory or library is an error.
The rbs command
The rbs command works on the files in sig/ plus the core and standard-library signatures that come with the gem. -I sig adds a directory, -r library adds a standard-library signature (such as -r json), and --no-stdlib removes the defaults. The bundled standard-library directory covers json, time, logger, optparse and many more. In RBS 3.10 Set is part of the core signatures and needs no -r.
$ rbs -I sig validate
$ echo $?
0
$ rbs -I sig list | grep Shelf
::Shelf (module)
::Shelf::Error (class)
::Shelf::Isbn (class)
::Shelf::Author (class)
::Shelf::Book (class)
::Shelf::Loan (class)
::Shelf::Store (module)
::Shelf::Store::Memory (class)
::Shelf::Catalog (class)
::Shelf::Config (class)
::Shelf::Store::_Store (interface)
Source: RBS repository: README (CLI)
validate is silent when the files are consistent and exits with a non-zero status when they are not. It checks that every referenced type exists, that generics are applied with the right number of arguments, and that variance rules hold. The query commands answer questions about the resolved environment.
$ rbs -I sig ancestors ::Shelf::Catalog
::Shelf::Catalog
::Enumerable[::Shelf::Book]
::Object
::Kernel
::BasicObject
$ rbs -I sig method ::Shelf::Catalog each
::Shelf::Catalog#each
defined_in: ::Shelf::Catalog
implementation: ::Shelf::Catalog
accessibility: public
types:
() { (::Shelf::Book) -> void } -> void at sig/shelf.rbs:65:14...65:43
| () -> ::Enumerator[::Shelf::Book, void] at sig/shelf.rbs:66:14...66:42
Source: RBS repository: docs/tools.md
The other subcommands are parse (syntax-check one file), ast, methods, constant, paths (the directories the environment loads), annotate (copy documentation from RDoc into signatures), subtract, diff, vendor and version.
Errors you will see
A syntax error names the exact position. This is also what happens when an interface is declared without the leading underscore, which is a common slip because classes and modules are written without one:
interface Store
def all: () -> Array[String]
end
$ rbs parse store.rbs
store.rbs:1:10...1:15: Syntax error: expected one of interface name, token=`Store` (tUIDENT) (RBS::ParsingError)
interface Store
^^^^^
Source: RBS repository: docs/syntax.md
The fix is interface _Store. An unknown type is a different error, reported by validate:
$ rbs -I bad2 validate
ERROR -- rbs: bad2/undef.rbs:2:12...2:16: Could not find Item (RBS::NoTypeFoundError)
def add: (Item item) -> void
^^^^
Source: RBS repository: README (CLI)
A name that RBS cannot resolve inside a namespace is looked up from the innermost scope outward, so the usual fix is a missing declaration or a missing -r for a standard library.
Generating a first draft
Writing signatures for existing code by hand is slow, so rbs prototype writes drafts. prototype rb reads source and prototype runtime loads the code and inspects live classes. Both produce a lot of untyped, which is the point: they give you the shape, and you replace untyped with types.
$ rbs prototype rb fines.rb
# Late fees for overdue loans.
class Fine
@days: untyped
@rate: untyped
def initialize: (untyped days, untyped rate) -> void
def amount: () -> untyped
def label: () -> ("overdue" | nil)
end
Source: RBS repository: README (CLI)
prototype rb guesses only from literals, so label shows the literal string type. prototype runtime sees the classes that Data.define built, which static reading cannot:
$ rbs prototype runtime -R lib/shelf.rb Shelf::Isbn
module Shelf
class Isbn < ::Data
def self.new: (untyped digits) -> instance
| (digits: untyped) -> instance
def self.[]: (untyped digits) -> instance
| (digits: untyped) -> instance
def self.members: () -> [ :digits ]
def members: () -> [ :digits ]
attr_reader digits: untyped
end
end
Checking signatures against the running program
Signatures can be wrong, and validate only checks that they are consistent with each other. rbs test closes the gap: it wraps the methods of the classes you name and checks every call and result against the signature while your normal test suite runs. Nothing is changed in the code, and you pay the cost only in the CI job that asks for it.
$ rbs -I sig test --target 'Shelf::*' ruby -Ilib -Itest test/shelf_test.rb
Run options: --seed 3027
# Running:
....
Finished in 0.000751s, 5326.2316 runs/s, 6657.7895 assertions/s.
4 runs, 5 assertions, 0 failures, 0 errors, 0 skips
Source: RBS repository: docs/sigs.md
A wrong signature is caught by a test that was green before. Here the return type of describe was deliberately changed to Integer, and the same tests were run through the lower-level setup (-rrbs/test/setup with the RBS_TEST_TARGET, RBS_TEST_OPT and RBS_TEST_RAISE environment variables, which is what rbs test configures for you):
1) Error:
ShelfTest#test_loan_pattern_matching:
RuntimeError: Neutered Exception RBS::Test::Tester::TypeError: TypeError: [Shelf::Loan#describe] ReturnTypeError: expected `::Integer` but returns `"ruby basics is lent"`
The error kinds are documented: ArgumentTypeError, BlockArgumentTypeError, ArgumentError, ReturnTypeError, BlockReturnTypeError, UnexpectedBlockError, MissingBlockError, UnresolvedOverloadingError and DuplicatedMethodDefinitionError. The --sample-size option limits how many elements of large collections are examined, and --unchecked-class excludes a class.
Source: RBS repository: docs/sigs.md
A Rake task keeps the cheap check in the default task:
# frozen_string_literal: true
require "rake"
desc "Validate the RBS signatures"
task :rbs do
sh "rbs", "-I", "sig", "validate"
end
task default: :rbs
Source: Rake documentation
Signatures for gems you depend on: rbs collection
rbs collection is, in the words of its documentation, Bundler for RBS. It reads Gemfile.lock, downloads signatures for the locked gems from a git repository of community signatures (ruby/gem_rbs_collection by default) into .gem_rbs_collection/ and records the choice in rbs_collection.lock.yaml. It needs git and a Gemfile.lock.
$ rbs collection init
created: rbs_collection.yaml
# Download sources
sources:
- type: git
name: ruby/gem_rbs_collection
remote: https://github.com/ruby/gem_rbs_collection.git
revision: main
repo_dir: gems
# A directory to install the downloaded RBSs
path: .gem_rbs_collection
# gems:
# - name: GEM_NAME
# ignore: true
rbs collection install and rbs collection update fetch from the network and were not run for this page; init was. Once installed, rbs and Steep load the signatures automatically, and rbs collection clean removes the unused ones. Add .gem_rbs_collection/ to .gitignore. Gems that ship their own sig/ directory need no collection entry.
TypeProf: inferring the types
TypeProf is, in its own words, an experimental type-level Ruby interpreter. It runs the program in the abstract, replacing each value with its class, and reports the argument and result types of every method it saw called. Its README states that it supports Ruby 3.3 or later. The input is plain Ruby, so it works even with no signatures at all. The more the entry point calls the code, the more it learns: a method that nothing calls is reported with no types, because the analysis starts from the top-level calls.
The script below has a class and two top-level calls that drive the analysis.
# frozen_string_literal: true
# Late fees for overdue loans.
class Fine
def initialize(days, rate)
@days = days
@rate = rate
end
def amount
@days * @rate
end
def label
@days > 14 ? "overdue" : nil
end
end
Fine.new(3, 0.25).amount
Fine.new(20, 1).label
$ typeprof fines.rb
# TypeProf 0.31.1
# fines.rb
class Fine
def initialize: (Integer, Float | Integer) -> void
def amount: -> (Float | Integer)
def label: -> String?
end
Source: TypeProf repository: doc/doc.md
Float | Integer is exactly what the program does: the first call passes a Float, the second an Integer, and amount returns whichever multiplication results. String? is the union with nil. The output is RBS, so it can be redirected to a draft signature with typeprof sig/app.rbs app.rb -o sig/app.gen.rbs, the use the documentation names as typical.
TypeProf can also point at mistakes. The next script calls a method on a value that may be nil, calls a method that does not exist, and passes the wrong number of arguments:
# frozen_string_literal: true
# A fine calculator with a deliberate mistake.
class Fine
def initialize(days, rate)
@days = days
@rate = rate
end
def amount = @days * @rate
def label = @days > 14 ? "overdue" : nil
end
fine = Fine.new(20, 1)
puts fine.label.upcase
puts fine.total
Fine.new(1)
$ typeprof --show-errors --show-parameter-names fines2.rb
# TypeProf 0.31.1
# fines2.rb
# (16,16)-(16,22):undefined method: nil#upcase
# (17,0)-(17,4):wrong type of arguments
# (17,10)-(17,15):undefined method: Fine#total
# (18,5)-(18,8):wrong number of arguments (1 for 2)
class Fine
def initialize: (Integer days, Integer rate) -> void
def amount: -> Integer
def label: -> String?
end
Source: TypeProf repository: doc/doc.md
Running the file on Ruby shows why the first finding matters, because label returns a String for 20 days and the crash happens later, on a different line:
$ ruby -w fines2.rb
OVERDUE
fines2.rb:17:in '<main>': undefined method 'total' for an instance of Fine (NoMethodError)
puts fine.total
^^^^^^
Source: TypeProf repository
Given an existing signature, TypeProf reads it and checks the calls against it. With the shelf signature loaded, a client script that passes nil where a Book is declared gets two findings on the Loan.new calls:
# frozen_string_literal: true
def titles(catalog) = catalog.map(&:title)
def first_loan(book)
Shelf::Loan.new(book, state: :lent)
end
titles(Shelf::Catalog.new)
first_loan(nil)
Shelf::Loan.new(nil, state: :stolen)
$ typeprof --show-errors client.rb sig/shelf.rbs
# TypeProf 0.31.1
# client.rb
# (6,14)-(6,17):wrong type of arguments
# (11,12)-(11,15):wrong type of arguments
class Object
def titles: (Shelf::Catalog) -> Array[untyped]
def first_loan: (nil) -> Shelf::Loan
end
Source: TypeProf repository
Three practical cautions from using it:
-
The repository’s own configuration file says that TypeProf reports many false positives and recommends the severity
hintornonefor editors. Treat the CLI findings as hints, not as a gate. -
Inference follows only the calls it can see. It does not replace writing the signature for a public API.
-
On the machine used for this page,
typeprof 0.31.1did not finish when a second, newerrbsgem (4.2.0) was installed next to the bundled 3.10.0; pinningrbsto 3.10.0 made it run. This was observed once and is stated as an observation; keep therbsandtypeprofversions in theGemfilealigned.
The same engine runs as a language server (typeprof --lsp, set up with typeprof --init and the Ruby TypeProf extension for Visual Studio Code), which shows inferred types and errors in the editor.
rbs-inline: types in comments
rbs-inline (community, Soutaro Matsumoto) lets you write the signature in comments above the code and generates the .rbs files from them. It is a way to avoid keeping two files in step. Its README describes the project as a prototype that is not actively updated, which is a reason to weigh it before adopting it. The listing below follows the syntax in that README and was syntax-checked with ruby -wc but not processed by rbs-inline, which is not installed.
# frozen_string_literal: true
# rbs_inline: enabled
module Shelf
# A late fee for an overdue loan.
class Fine
attr_reader :days #: Integer
# @rbs days: Integer
# @rbs rate: Float
# @rbs return: void
def initialize(days, rate)
@days = days
@rate = rate
end
#: () -> Float
def amount = @days * @rate
#: () -> String?
def label = @days > 14 ? "overdue" : nil
end
end
Generating the files is a single command, bundle exec rbs-inline --output lib, which writes under sig/generated (without --output it prints to the terminal). The magic comment # rbs_inline: enabled opts a file in. Note that RuboCop 1.92 reports Layout/LeadingCommentSpace on the #: form, because the comment has no space after the hash; either exclude the cop for those files or use the # @rbs form.
Source: rbs-inline repository
Steep: a static checker for RBS
Steep (community, Soutaro Matsumoto) type-checks Ruby files against the RBS signatures. It is installed with gem install steep or from the Gemfile, and configured in a Steepfile, which is Ruby code. The file below was checked with ruby -wc and RuboCop; Steep itself was not run.
# frozen_string_literal: true
target :lib do
signature "sig"
check "lib"
library "json"
end
The commands, from the Steep README, are steep init (generate a Steepfile) and steep check, which prints diagnostics such as MethodDefinitionMissing with the file and position. Signatures for locked gems come through rbs collection; the README says to add library only for signatures that the collection does not manage, and the library "json" line above shows the form for a standard library.
Source: Steep repository
Two things to know before adopting Steep:
-
It checks the code against the signature, so it needs signatures for the code it checks. Start by checking one directory and grow the
checklist. -
Some Ruby constructs confuse any static checker. The RBS documentation gives an example that matters for this gem:
Measure = Data.define(...)has the type of the anonymous class thatData.definereturns, not the class named in the signature, which is reported as a type error. The documented remedies are to assign to_(Measure = _ = Data.define(...)) or to inherit fromData.define(...), at the cost of an extra anonymous class inancestors. The shelf gem uses the plain form, so Steep would need that adjustment.
Sorbet: types in the code
Sorbet (community, built at Stripe) takes the opposite route to RBS: the types are Ruby code. A method declares its signature with sig, a block that is run, so that the sorbet-runtime gem can check the types at run time, while the sorbet gem checks them statically. The listing below is described from the Sorbet documentation and was not run, since neither gem is installed here; it passes ruby -wc.
# frozen_string_literal: true
# typed: strict
require "sorbet-runtime"
module Shelf
# A loan as a typed struct.
class LoanRecord < T::Struct
const :title, String
prop :state, Symbol
const :due_in, T.nilable(Integer)
end
# A late fee with a runtime-checked signature.
class Fine
extend T::Sig
sig { params(days: Integer, rate: Float).void }
def initialize(days, rate)
@days = days
@rate = rate
end
sig { returns(Float) }
def amount = @days * @rate
end
end
Source: Sorbet: Type annotations (sig)
-
extend T::Sigadds thesigmethod to the class. The block usesparams(...),returns(...)orvoid, and everysigneeds areturnsorvoid. -
T::Structdefines a class with typedconst(read-only) andprop(read-write) attributes. Construction takes keyword arguments only, and both are required unless there is adefault:, afactory:or aT.nilabletype. It plays the role thatData.defineplays elsewhere, with types checked at run time. -
T.nilable(Integer)is the optional type that RBS writesInteger?;T.untypedisuntyped. -
The
# typed:comment sets the strictness of each file. From the Sorbet documentation:ignore(the file is not read),false(the default; only syntax, constant resolution andsigcorrectness are reported),true(type errors are also reported),strict(methods need asigand constants and instance variables need explicit types) andstrong(noT.untypedallowed; in beta). The ability to raise the level file by file is how Sorbet does gradual adoption. -
The documented set-up adds
sorbet(development),sorbet-runtimeandtapiocato theGemfile, runsbundle exec tapioca initto create thesorbet/directory (which contains RBI files for gems and generated constants) and checks withsrb tc.
Sorbet’s RBI files and RBS files are different formats for similar purposes; a project normally picks one of the two toolchains rather than combining them.
Gradual typing and its trade-offs
Both families are gradual: they check what is described and trust untyped for the rest. The real decision is what you want the types to do.
| Concern | Signatures in files (RBS with Steep) | Annotations in code (Sorbet) |
|---|---|---|
Ruby code stays unchanged |
Yes |
No, |
Types usable by non-users of the checker |
Yes, the code is plain Ruby |
The gem needs |
Runtime checking |
Optional, |
Built in, with a performance cost per call |
Ships with Ruby |
RBS yes, Steep no |
No |
Metaprogramming, |
Describe the generated API by hand |
Generated RBI files, with |
Gradual by |
|
The |
Reasons to adopt types at all:
-
A public API of a gem gets machine-readable documentation, and editors can complete from the signatures.
-
Refactors become safer when a checker finds every caller that breaks.
-
The cost: signature files drift from the code (which
rbs testand a checker catch), and Ruby’s open classes,Data.define, refinements and generated methods need workarounds.
Reasons to stop at tests: for a small script or an application with strong tests, a Minitest suite plus RuboCop catches most defects, and neither family of tools makes a failing test unnecessary. A common middle path is RBS for the public API of a gem, rbs validate and rbs test in CI, and no static checker.
Where older material is outdated
One of the books used to cross-check this reference includes an RBS example whose interface name has no leading underscore, which the parser rejects. Beyond that case, these are the points that older tutorials commonly get out of date.
| Older material may say | In Ruby 4.0 |
|---|---|
An RBS interface is declared with a plain capitalised name, as a class would be. |
The parser requires a leading underscore ( |
RBS is new or optional tooling that you install. |
It is a bundled gem (3.10.0 in Ruby 4.0), with a full command line, |
Value objects are written |
|
|
|
References
-
Ruby 4.0 NEWS, for the bundled gem versions
-
rbs-inline (community)
-
Steep (community)
-
Sorbet documentation, sig, strictness levels, T::Struct and adopting Sorbet (community)