Sorbet Cheatsheet Part 3

Published Nov 15, 2020

Continuing from last time.

Change from last time: I filled in the standard library helpers.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# Every file should have a "typed sigil" that tells Sorbet how strict to be
# during static type checking.
#
# Strictness levels (lax to strict):
#
# ignore: Sorbet won't even read the file.  This means its contents are not
# visible during type checking.  Avoid this.
#
# false: Sorbet will only report errors related to constant resolution.  This
# is the default if no sigil is included.
#
# true: Sorbet will report all static type errors.  This is the sweet spot of
# safety for effort.
#
# strict: Sorbet will require that all methods, constants, and instance
# variables have static types.
#
# strong: Sorbet will no longer allow anything to be T.untyped, even
# explicitly.  Almost nothing satisfies this.

# typed: true

# Include the runtime type-checking library.  This lets you write inline sigs
# and have them checked at runtime (instead of running Sorbet as RBI-only).
# These runtime checks happen even for files with `ignore` or `false` sigils.
require 'sorbet-runtime'

class BasicSigs
  # Bring in the type definition helpers.  You'll almost always need this.
  extend T::Sig

  # Sigs are defined with `sig` and a block.  Define the return value type with
  # `returns`.
  #
  # This method returns a value whose class is `String`.  These are the most
  # common types, and Sorbet calls them "class types".
  sig { returns(String) }
  def greet
    'Hello, World!'
  end

  # Define parameter value types with `params`.
  sig { params(n: Integer).returns(String) }
  def greet_repeat(n)
    (1..n).map { greet }.join("\n")
  end

  # Define keyword parameters the same way.
  sig { params(n: Integer, sep: String).returns(String) }
  def greet_repeat(n, sep: "\n")
    (1..n).map { greet }.join(sep)
  end

  # Notice that positional/keyword and required/optional make no difference
  # here.  They're all defined the same way in `params`.

  # For lots of parameters, it's nicer to use do..end and a multiline block
  # instead of curly braces.
  sig do
    params(
      str: String,
      num: Integer,
      sym: Symbol,
    ).returns(String)
  end
  def uhh(str:, num:, sym:)
    'What would you even do with these?'
  end

  # For a method whose return value is useless, use `void`.
  sig { params(name: String).void }
  def say_hello(name)
    puts "Hello, #{name}!"
  end

  # Splats! Also known as "rest parameters", "*args", "**kwargs", and others.
  #
  # Type the value that a _member_ of `args` or `kwargs` will have, not `args`
  # or `kwargs` itself.
  sig { params(args: Integer, kwargs: String).void }
  def no_op(*args, **kwargs)
    if kwargs[:op] == 'minus'
      args.each { |i| puts(i - 1) }
    else
      args.each { |i| puts(i + 1) }
    end
  end

  # Most initializers should be `void`.
  sig { params(name: String).void }
  def initialize(name:)
    # Instance variables must have annotated types to participate in static
    # type checking.

    # The value in `T.let` is checked statically and at runtime.
    @upname = T.let(name.upcase, String)

    # Sorbet can infer this one!
    @name = name
  end

  # Constants also need annotated types.
  SORBET = T.let('A delicious frozen treat', String)

  # Class variables too.
  @@the_answer = T.let(42, Integer)
end

class StandardHelpers
  # Sorbet provides some helpers for typing the Ruby standard library.

  # Use T::Boolean to catch both `true` and `false`.
  #
  # For the curious, this is equivalent to
  #     T.type_alias { T.any(TrueClass, FalseClass) }
  sig { params(str: String).returns(T::Boolean) }
  def confirmed?(str)
    str == 'yes'
  end

  # Reminder that the value `nil` is an instance of NilClass.
  sig { params(val: NilClass).void }
  def only_nil(val:); end

  # To avoid modifying common standard library classes, Sorbet provides
  # wrappers to support common generics.
  #
  # Here's the full list:
  #   * T::Array
  #   * T::Enumerable
  #   * T::Enumerator
  #   * T::Hash
  #   * T::Range
  #   * T::Set
  sig { params(config: T::Hash[Symbol, String]).returns(T::Array[String]) }
  def merge_values(config)
    keyset = [:old_key, :new_key]
    config.each_pair.flat_map do |key, value|
      keyset.include?(key) ? value : nil
    end
  end

  # Sometimes (usually dependency injection), a method will accept a reference
  # to a class rather than an instance of the class.  Use `T.class_of(Dep)` to
  # accept the `Dep` class itself (or something that inherits from it).
  class Dep; end

  sig { params(dep: T.class_of(Dep)).returns(Dep) }
  def dependency_injection(dep:)
    dep.new
  end

  # Blocks, procs, and lambdas, oh my!  All of these are typed with `T.proc`.
  #
  # Limitations:
  # 1. All parameters are assumed to be required positional parameters.
  # 2. The only runtime check is that the value is a `Proc`.  The argument
  #    types are only checked statically.
  sig do
    params(
      data: T::Array[String],
      blk: T.proc.params(val: String).returns(Integer),
    ).returns(Integer)
  end
  def count(data, &blk)
    data.sum(&blk)
  end

  sig { returns(Integer) }
  def count_usage
    count(["one", "two", "three"]) { |word| word.length + 1 }
  end

  # If the method takes an implicit block, Sorbet will infer `T.untyped` for
  # it.  Use the explicit block syntax if the types are important.
  sig { params(str: String).returns(T.untyped) }
  def implicit_block(str)
    yield(str)
  end

  # If you're writing a DSL and will execute the block in a different context,
  # use `bind`.
  sig { params(num: Integer, blk: T.proc.bind(Integer).void).void }
  def number_fun(num, &blk)
    num.instance_eval(&blk)
  end

  sig { void }
  def number_fun_usage(num)
    number_fun(10) { puts digits.join }
  end

  # If the block doesn't take any parameters, don't include `params`.
  sig { params(blk: T.proc.returns(Integer)).returns(Integer) }
  def doubled_block(&blk)
    2 * blk.call
  end
end

class Combinators
  # TODO: T.any
  # TODO: T.all
  # TODO: T.nilable
  # TODO: stacking T.nilable
end

class DataClasses
  # TODO: T::Struct
  # TODO: ^ reference equality, not value equality
  # TODO: T::Enum
end

class FlowSensitivity
  # TODO: T.absurd
  # TODO: T.noreturn
end

class Metaprogramming
  # TODO: T.type_alias
  # TODO: T.self_type
  # TODO: T.attached_class
end

class InheritanceChecks
  # TODO: abstract!
  # TODO: interface!
  # TODO: abstract. / override.
  # TODO: mixes_in_class_methods
  # TODO: final!
  # TODO: sealed!
end

class Debugging
  # TODO: T.reveal_type
end

class EscapeHatches
  # TODO: T.untyped
  # TODO: T.cast
  # TODO: T.unsafe
  # TODO: T.must
  # TODO: T.assert_type!
end

# The following types are not officially documented but are still useful.

class ValueSet
  # TODO: T.enum
end

class Generics
  # TODO: type_parameters / T.type_parameter
  # TODO: T::Generic
  # TODO: type_member
end