In Ruby's RBS signature language, how do you declare a class's methods, including optional, keyword, nilable and block parameters?
answer
- separate .rbs files under sig/
- def name: (params) -> return
- ?opt, key:, ?key: keyword
- Type? means Type or nil
- { (Elem) -> void } for a block
basics
~20 sRBS declares Ruby types in separate .rbs files, usually under sig/: def name: (Integer, ?String, key: Symbol) -> String. A leading ? marks an optional parameter, a trailing ? a nilable type, and { (T) -> void } a block.
solid answer
~40 sAn RBS file mirrors the Ruby structure: `class Billing::Invoice ... end` containing members. A method is `def add_line: (String description, Integer cents, ?quantity: Integer) -> self`: positional types in order, `?` before an optional positional or keyword, `key: Type` for a required keyword, `*T` and `**T` for rest parameters, and the return type after `->`. `LineItem?` means `LineItem | nil`, `|` builds unions, `Array[LineItem]` and `Hash[String, Integer]` are generics, `bool`, `void` and `untyped` are built in. A block is written before the arrow, `() { (LineItem) -> void } -> self`, with `?{ ... }` when it is optional, and overloads are joined with `|`. Instance variables are `@paid_at: Time?`, attributes `attr_reader total_cents: Integer`, class methods `def self.load: (String) -> Invoice`. `rbs -I sig validate` checks the files.
code
ruby · 15 linesmodule Billing
class Invoice
attr_reader :customer_id, :lines
def initialize(customer_id:, currency: :usd)
@customer_id = customer_id
@currency = currency
@lines = []
end
def find_line(description)
lines.find { it.description == description }
end
end
endgo deeper
Recall that RBS lives in .rbs files under sig/ and that a method is written def name: (ParamTypes) -> ReturnType.
Explain the parameter forms, ?optional, key:, ?key:, *rest and **rest, the difference between ?T and T?, block syntax and overloads with |.
Show judgement in the signatures: interfaces for duck types, void versus a real return type, generics instead of untyped, and rbs validate in the build.
Treat signatures as a published contract: once sig/ ships in a gem, callers type-check against it, so signature changes need the same review as API changes.
## What RBS is **RBS** is Ruby's language for describing types. It is not Ruby syntax and Ruby never reads it while your program runs: signatures live in **separate `.rbs` files**, conventionally in a `sig/` directory that mirrors `lib/`, and tools such as Steep, TypeProf and editors read them. RBS has shipped with Ruby as a bundled gem since Ruby 3.0, and the gem's own signatures for core classes and the standard library come with it. ## Declarations and members A file contains **declarations**: `class`, `module`, `interface`, `type` aliases, constants and globals. Inside a class you write **members**: - `@paid_at: Time?` declares an instance variable. - `attr_reader total_cents: Integer` declares the reader method and its `@total_cents` variable. - `def name: METHOD-TYPE` declares an instance method; `def self.name:` a singleton (class) method; `def self?.name:` a `module_function`. - `include`, `extend` and `prepend` declare mixins; `private` and `public` change the visibility of the following definitions. ## The method type A method type reads `(parameters) block -> return`: | Ruby parameter | RBS form | |---|---| | required positional | `Integer` or `Integer cents` | | optional positional | `?Integer` | | rest positional | `*String` | | required keyword | `currency: Symbol` | | optional keyword | `?currency: Symbol` | | rest keywords | `**untyped` | | block | `{ (LineItem) -> void }`, or `?{ ... }` if optional | Parameter names are optional documentation. Overloads are joined with `|`, and a return type that is itself a union needs parentheses: `-> (Integer | Float)`. ## The types you use most 1. **Class instance types**: `String`, `Billing::LineItem`. 2. **Generics**: `Array[LineItem]`, `Hash[String, Integer]`, `Enumerator[LineItem, self]`. 3. **Optional types**: `LineItem?` is `LineItem | nil`. Watch the position: `?Integer` in a parameter list means *may be omitted*, `Integer?` means *may be nil*, and `?Integer?` means both. 4. **Unions and literals**: `:usd | :eur` is a union of two symbol literals. 5. **Records and tuples**: `{ id: Integer, name: String }` is a Hash with fixed keys, `[Integer, String]` a fixed-size Array. 6. **Interfaces**: an `interface _ToCents` declaration with a `def to_cents: () -> Integer` member describes a duck type; interface names start with `_`. 7. **Base types**: `bool` (`true | false`), `boolish` (any value used as a condition), `void` (a result you should not use), `self`, `instance`, `nil`, and `untyped`, which switches checking off. ## A billing example ```rbs type Billing::currency = :usd | :eur | :gbp module Billing class Invoice attr_reader customer_id: String attr_reader lines: Array[LineItem] @paid_at: Time? def initialize: (customer_id: String, ?currency: Billing::currency) -> void def add_line: (String description, Integer cents, ?quantity: Integer) -> self def find_line: (String description) -> LineItem? def paid?: () -> bool def each_line: () { (LineItem) -> void } -> self | () -> Enumerator[LineItem, self] def self.load: (String json) -> Invoice end end ``` `initialize` returns `void` because its value is never used; `each_line` is overloaded for the block and no-block calls, just as `Array#each` is in the core signatures. ## Checking the file itself - `rbs -I sig validate` parses the files and checks that every name they mention resolves. - `rbs -I sig method Billing::Invoice add_line` prints the resolved method type. - A gem ships its signatures by packaging `sig/`; RBS loads it automatically when another project uses the gem as a library. ## Common mistakes - Writing `Integer | nil` everywhere instead of `Integer?`, or confusing `?Integer` with `Integer?`. - Declaring a keyword argument positionally, `(Symbol currency)`, which describes a different call. - Reaching for `untyped` so often that the checker has nothing to check.
- What is the difference between `?String` and `String?` in an RBS method type?`?String` in a parameter list marks an **optional positional parameter**: the caller may omit it. `String?` is an **optional type**, `String | nil`: the value may be nil. `(?String? name)` combines them, so the argument may be left out, given a String, or given nil.
- Why would you declare an interface such as `_ToCents` instead of a class type?An interface describes behaviour, not ancestry: `interface _ToCents` with `def to_cents: () -> Integer` accepts any object that has that method, so `Money`, `Integer` wrappers or test doubles all fit without sharing a superclass. It is how RBS expresses Ruby's duck typing in a signature.
saying these in an interview costs you the question
- RBS type annotations are written inside the Ruby method definitions themselves.
- Ruby checks RBS signatures on every method call at runtime.
- ?Integer in a parameter list means the argument may be nil.
- Keyword arguments are declared positionally, in the order they are passed.
- untyped means the value is always nil.
- A block must be declared as an extra &block parameter type.