RBS By Example

Goal

The purpose of this doc is to teach you how to write RBS signatures by using the standard library's methods as a guide.

Examples

Zero argument methods

Example: String#empty?

# .rb
"".empty?
# => true
"hello".empty?
# => false
# .rbs
class String
  def empty?: () -> bool
end

String's #empty method takes no parameters, and returns a boolean value

Single argument methods

Example: String#include?

# .rb
"homeowner".include?("house")
# => false
"homeowner".include?("meow")
# => true
class String
  def include?: (String) -> bool
end

String's include? method takes one argument, a String, and returns a boolean value

Variable argument methods

Example: String#end_with?

# .rb
"hello?".end_with?("!")
# => false
"hello?".end_with?("?")
# => true
"hello?".end_with?("?", "!")
# => true
"hello?".end_with?(".", "!")
# => false
# .rbs
class String
  def end_with?: (*String) -> bool
end

String's #end_with? method takes any number of String arguments, and returns a boolean value.

Optional positional arguments

Example: String#ljust

# .rb
"hello".ljust(4)
#=> "hello"
"hello".ljust(20)
#=> "hello               "
"hello".ljust(20, '1234')
#=> "hello123412341234123"
# .rbs
class String
  def ljust: (Integer, ?String) -> String
end

String's ljust takes one Integer argument, and an optional String argument, indicated by the the ? prefix marker. It returns a String.

Multiple signatures for a single method

Example: Array#*

# .rb
[1, 2, 3] * ","
# => "1,2,3"
[1, 2, 3] * 2
# => [1, 2, 3, 1, 2, 3]

Note: Some of the signatures after this point include type variables (e.g. Elem, T). For now, it's safe to ignore them, but they're included for completeness.

# .rbs
class Array[Elem]
  def *: (String) -> String
       | (Integer) -> Array[Elem]
end

Array's * method, when given a String returns a String. When given an Integer, it returns an Array of the same contained type Elem (in our example case, Elem corresponds to Integer).

Union types

Example: String#<<

# .rb
a = "hello "
a << "world"
#=> "hello world"
a << 33
#=> "hello world!"
# .rbs
class String
  def <<: (String | Integer) -> String
end

String's << operator takes either a String or an Integer, and returns a String.

Nilable types

# .rb
[1, 2, 3].first
# => 1
[].first
# => nil
[1, 2, 3].first(2)
# => [1, 2]
[].first(2)
# => []
# .rbs
class Enumerable[Elem]
  def first: () -> Elem?
           | (Integer) -> Array[Elem]
end

Enumerable's #first method has two different signatures.

When called with no arguments, the return value will either be an instance of whatever type is contained in the enumerable, or nil. We represent that with the type variable Elem, and the ? suffix nilable marker.

When called with an Integer positional argument, the return value will be an Array of whatever type is contained.

The ? syntax is a convenient shorthand for a union with nil. An equivalent union type woould be (Elem | nil).

Keyword Arguments

Example: String#lines

# .rb
"hello\nworld\n".lines
# => ["hello\n", "world\n"]
"hello  world".lines(' ')
# => ["hello ", " ", "world"]
"hello\nworld\n".lines(chomp: true)
# => ["hello", "world"]
# .rbs
class String
  def lines: (?String, ?chomp: bool) -> Array[String]
end

String's #lines method take two arguments: one optional String argument, and another optional boolean keyword argument. It returns an Array of Strings.

Keyword arguments are declared similar to in ruby, with the keyword immediately followed by a colon. Keyword arguments that are optional are indicated as optional using the same ? prefix as positional arguments.

Class methods

Example: Time.now

# .rb
Time.now
# => 2009-06-24 12:39:54 +0900
class Time
  def self.now: () -> Time
end

Time's class method now takes no arguments, and returns an instance of the Time class.

Block Arguments

Example: Array#filter

# .rb
[1,2,3,4,5].filter {|num| num.even? }
# => [2, 4]
%w[ a b c d e f ].filter {|v| v =~ /[aeiou]/ }
# => ["a", "e"]
[1,2,3,4,5].filter
# .rbs
class Array[Elem]
  def filter: () { (Elem) -> boolish } -> ::Array[Elem]
            | () -> ::Enumerator[Elem, ::Array[Elem]]
end

Array's #filter method, when called with no arguments returns an Enumerator.

When called with a block, the method returns an Array of whatever type the original contained. The block will take one argument, of the type of the contained value, and the block will return a truthy or falsy value.

boolish is a special keyword for any type that will be treated as if it were a bool.

Type Variables

Example: Hash, Hash#keys

h = { "a" => 100, "b" => 200, "c" => 300, "d" => 400 }
h.keys
# => ["a", "b", "c", "d"]
# .rbs
class Hash[K, V]
  def keys: () -> Array[K]
end

Generic types in RBS are parameterized at declaration time. These type variables are then available throughout all the methods contained in the class block.

Hash's #keys method takes no arguments, and returns an Array of the first type parameter. In the above example, a is of concrete type Hash[String, Integer], so #keys returns an Array for String.

# .rb
a = [ "a", "b", "c", "d" ]
a.collect {|x| x + "!"}
# => ["a!", "b!", "c!", "d!"]
a.collect.with_index {|x, i| x * i}
# => ["", "b", "cc", "ddd"]
# .rbs
class Array[Elem]
  def collect: [U] () { (Elem) -> U } -> Array[U]
             | () -> Enumerator[Elem, Array[untyped]]
end

Type variables can also be introduced in methods. Here, in Array's #collect method, we introduce a type variable U. The block passed to #collect will receive a parameter of type Elem, and return a value of type U. Then #collect will return an Array of type U.

In this example, the method receives its signature from the inferred return type of the passed block. When then block is absent, as in when the method returns an Enumerator, we can't infer the type, and so the return value of the enumerator can only be described as Array[untyped].

Tuples

Examples: Enumerable#partition, Enumerable#to_h

(1..6).partition { |v| v.even? }
# => [[2, 4, 6], [1, 3, 5]]
class Enumerable[Elem]
  def partition: () { (Elem) -> boolish } -> [Array[Elem], Array[Elem]]
               | () -> ::Enumerator[Elem, [Array[Elem], Array[Elem] ]]
end

Enumerable's partition method, when given a block, returns a 2-item tuple of Arrays containing the original type of the Enumerable.

Tuples can be of any size, and they can have mixed types.

(1..5).to_h {|x| [x, x ** 2]}
# => {1=>1, 2=>4, 3=>9, 4=>16, 5=>25}
class Enumerable[Elem]
  def to_h: () -> ::Hash[untyped, untyped]
          | [T, U] () { (Elem) -> [T, U] } -> ::Hash[T, U]
end

Enumerable's to_h method, when given a block that returns a 2-item tuple, returns a Hash with keys the type of the first position in the tuple, and values the type of the second position in the tuple.