Sorbet is a type checker for Ruby. It adds syntax for method signatures that enable both static and runtime type checking.
The easiest way to see it in action is in the playground at sorbet.run.
Try copying in one of the sections below! Each top-level class
or module
is independent from the others.
1# Every file should have a "typed sigil" that tells Sorbet how strict to be
2# during static type checking.
3#
4# Strictness levels (lax to strict):
5#
6# ignore: Sorbet won't even read the file. This means its contents are not
7# visible during type checking. Avoid this.
8#
9# false: Sorbet will only report errors related to constant resolution. This is
10# the default if no sigil is included.
11#
12# true: Sorbet will report all static type errors. This is the sweet spot of
13# safety for effort.
14#
15# strict: Sorbet will require that all methods, constants, and instance
16# variables have static types.
17#
18# strong: Sorbet will no longer allow anything to be T.untyped, even
19# explicitly. Almost nothing satisfies this.
20
21# typed: true
22
23# Include the runtime type-checking library. This lets you write inline sigs
24# and have them checked at runtime (instead of running Sorbet as RBI-only).
25# These runtime checks happen even for files with `ignore` or `false` sigils.
26require 'sorbet-runtime'
27
28class BasicSigs
29 # Bring in the type definition helpers. You'll almost always need this.
30 extend T::Sig
31
32 # Sigs are defined with `sig` and a block. Define the return value type with
33 # `returns`.
34 #
35 # This method returns a value whose class is `String`. These are the most
36 # common types, and Sorbet calls them "class types".
37 sig { returns(String) }
38 def greet
39 'Hello, World!'
40 end
41
42 # Define parameter value types with `params`.
43 sig { params(n: Integer).returns(String) }
44 def greet_repeat(n)
45 (1..n).map { greet }.join("\n")
46 end
47
48 # Define keyword parameters the same way.
49 sig { params(n: Integer, sep: String).returns(String) }
50 def greet_repeat_2(n, sep: "\n")
51 (1..n).map { greet }.join(sep)
52 end
53
54 # Notice that positional/keyword and required/optional make no difference
55 # here. They're all defined the same way in `params`.
56
57 # For lots of parameters, it's nicer to use do..end and a multiline block
58 # instead of curly braces.
59 sig do
60 params(
61 str: String,
62 num: Integer,
63 sym: Symbol,
64 ).returns(String)
65 end
66 def uhh(str:, num:, sym:)
67 'What would you even do with these?'
68 end
69
70 # For a method whose return value is useless, use `void`.
71 sig { params(name: String).void }
72 def say_hello(name)
73 puts "Hello, #{name}!"
74 end
75
76 # Splats! Also known as "rest parameters", "*args", "**kwargs", and others.
77 #
78 # Type the value that a _member_ of `args` or `kwargs` will have, not `args`
79 # or `kwargs` itself.
80 sig { params(args: Integer, kwargs: String).void }
81 def no_op(*args, **kwargs)
82 if kwargs[:op] == 'minus'
83 args.each { |i| puts(i - 1) }
84 else
85 args.each { |i| puts(i + 1) }
86 end
87 end
88
89 # Most initializers should be `void`.
90 sig { params(name: String).void }
91 def initialize(name:)
92 # Instance variables must have annotated types to participate in static
93 # type checking.
94
95 # The value in `T.let` is checked statically and at runtime.
96 @upname = T.let(name.upcase, String)
97
98 # Sorbet can infer this one!
99 @name = name
100 end
101
102 # Constants also need annotated types.
103 SORBET = T.let('A delicious frozen treat', String)
104
105 # Class variables too.
106 @@the_answer = T.let(42, Integer)
107
108 # Sorbet knows about the `attr_*` family.
109 sig { returns(String) }
110 attr_reader :upname
111
112 sig { params(write_only: Integer).returns(Integer) }
113 attr_writer :write_only
114
115 # You say the reader part and Sorbet will say the writer part.
116 sig { returns(String) }
117 attr_accessor :name
118end
119
120module Debugging
121 extend T::Sig
122
123 # Sometimes it's helpful to know what type Sorbet has inferred for an
124 # expression. Use `T.reveal_type` to make type-checking show a special error
125 # with that information.
126 #
127 # This is most useful if you have Sorbet integrated into your editor so you
128 # can see the result as soon as you save the file.
129
130 sig { params(obj: Object).returns(String) }
131 def debug(obj)
132 T.reveal_type(obj) # Revealed type: Object
133 repr = obj.inspect
134
135 # Remember that Ruby methods can be called without arguments, so you can
136 # save a couple characters!
137 T.reveal_type repr # Revealed type: String
138
139 "DEBUG: " + repr
140 end
141end
142
143module StandardLibrary
144 extend T::Sig
145 # Sorbet provides some helpers for typing the Ruby standard library.
146
147 # Use T::Boolean to catch both `true` and `false`.
148 #
149 # For the curious, this is equivalent to
150 #
151 # T.type_alias { T.any(TrueClass, FalseClass) }
152 #
153 sig { params(str: String).returns(T::Boolean) }
154 def confirmed?(str)
155 str == 'yes'
156 end
157
158 # Remember that the value `nil` is an instance of NilClass.
159 sig { params(val: NilClass).void }
160 def only_nil(val:); end
161
162 # To avoid modifying standard library classes, Sorbet provides wrappers to
163 # support common generics.
164 #
165 # Here's the full list:
166 # * T::Array
167 # * T::Enumerable
168 # * T::Enumerator
169 # * T::Hash
170 # * T::Range
171 # * T::Set
172 sig { params(config: T::Hash[Symbol, String]).returns(T::Array[String]) }
173 def merge_values(config)
174 keyset = [:old_key, :new_key]
175 config.each_pair.flat_map do |key, value|
176 keyset.include?(key) ? value : 'sensible default'
177 end
178 end
179
180 # Sometimes (usually dependency injection), a method will accept a reference
181 # to a class rather than an instance of the class. Use `T.class_of(Dep)` to
182 # accept the `Dep` class itself (or something that inherits from it).
183 class Dep; end
184
185 sig { params(dep: T.class_of(Dep)).returns(Dep) }
186 def dependency_injection(dep:)
187 dep.new
188 end
189
190 # Blocks, procs, and lambdas, oh my! All of these are typed with `T.proc`.
191 #
192 # Limitations:
193 # 1. All parameters are assumed to be required positional parameters.
194 # 2. The only runtime check is that the value is a `Proc`. The argument types
195 # are only checked statically.
196 sig do
197 params(
198 data: T::Array[String],
199 blk: T.proc.params(val: String).returns(Integer),
200 ).returns(Integer)
201 end
202 def count(data, &blk)
203 data.sum(&blk)
204 end
205
206 sig { returns(Integer) }
207 def count_usage
208 count(["one", "two", "three"]) { |word| word.length + 1 }
209 end
210
211 # If the method takes an implicit block, Sorbet will infer `T.untyped` for
212 # it. Use the explicit block syntax if the types are important.
213 sig { params(str: String).returns(T.untyped) }
214 def implicit_block(str)
215 yield(str)
216 end
217
218 # If you're writing a DSL and will execute the block in a different context,
219 # use `bind`.
220 sig { params(num: Integer, blk: T.proc.bind(Integer).void).void }
221 def number_fun(num, &blk)
222 num.instance_eval(&blk)
223 end
224
225 sig { params(num: Integer).void }
226 def number_fun_usage(num)
227 number_fun(10) { puts digits.join }
228 end
229
230 # If the block doesn't take any parameters, don't include `params`.
231 sig { params(blk: T.proc.returns(Integer)).returns(Integer) }
232 def doubled_block(&blk)
233 2 * blk.call
234 end
235end
236
237module Combinators
238 extend T::Sig
239 # These methods let you define new types from existing types.
240
241 # Use `T.any` when you have a value that can be one of many types. These are
242 # sometimes known as "union types" or "sum types".
243 sig { params(num: T.any(Integer, Float)).returns(Rational) }
244 def hundreds(num)
245 num.rationalize
246 end
247
248 # `T.nilable(Type)` is a convenient alias for `T.any(Type, NilClass)`.
249 sig { params(val: T.nilable(String)).returns(Integer) }
250 def strlen(val)
251 val.nil? ? -1 : val.length
252 end
253
254 # Use `T.all` when you have a value that must satisfy multiple types. These
255 # are sometimes known as "intersection types". They're most useful for
256 # interfaces (described later), but can also describe helper modules.
257
258 module Reversible
259 extend T::Sig
260 sig { void }
261 def reverse
262 # Pretend this is actually implemented
263 end
264 end
265
266 module Sortable
267 extend T::Sig
268 sig { void }
269 def sort
270 # Pretend this is actually implemented
271 end
272 end
273
274 class List
275 include Reversible
276 include Sortable
277 end
278
279 sig { params(list: T.all(Reversible, Sortable)).void }
280 def rev_sort(list)
281 # reverse from Reversible
282 list.reverse
283 # sort from Sortable
284 list.sort
285 end
286
287 def rev_sort_usage
288 rev_sort(List.new)
289 end
290
291 # Sometimes, actually spelling out the type every time becomes more confusing
292 # than helpful. Use type aliases to make them easier to work with.
293 JSONLiteral = T.type_alias { T.any(Float, String, T::Boolean, NilClass) }
294
295 sig { params(val: JSONLiteral).returns(String) }
296 def stringify(val)
297 val.to_s
298 end
299end
300
301module DataClasses
302 extend T::Sig
303 # Use `T::Struct` to create a new class with type-checked fields. It combines
304 # the best parts of the standard Struct and OpenStruct, and then adds static
305 # typing on top.
306 #
307 # Types constructed this way are sometimes known as "product types".
308
309 class Matcher < T::Struct
310 # Use `prop` to define a field with both a reader and writer.
311 prop :count, Integer
312 # Use `const` to only define the reader and skip the writer.
313 const :pattern, Regexp
314 # You can still set a default value with `default`.
315 const :message, String, default: 'Found one!'
316
317 # This is otherwise a normal class, so you can still define methods.
318
319 # You'll still need to bring `sig` in if you want to use it though.
320 extend T::Sig
321
322 sig { void }
323 def reset
324 self.count = 0
325 end
326 end
327
328 sig { params(text: String, matchers: T::Array[Matcher]).void }
329 def awk(text, matchers)
330 matchers.each(&:reset)
331 text.lines.each do |line|
332 matchers.each do |matcher|
333 if matcher.pattern =~ line
334 Kernel.puts matcher.message
335 matcher.count += 1
336 end
337 end
338 end
339 end
340
341 # Gotchas and limitations
342
343 # 1. `const` fields are not truly immutable. They don't have a writer method,
344 # but may be changed in other ways.
345 class ChangeMe < T::Struct
346 const :list, T::Array[Integer]
347 end
348
349 sig { params(change_me: ChangeMe).returns(T::Boolean) }
350 def whoops!(change_me)
351 change_me = ChangeMe.new(list: [1, 2, 3, 4])
352 change_me.list.reverse!
353 change_me.list == [4, 3, 2, 1]
354 end
355
356 # 2. `T::Struct` inherits its equality method from `BasicObject`, which uses
357 # identity equality (also known as "reference equality").
358 class Coordinate < T::Struct
359 const :row, Integer
360 const :col, Integer
361 end
362
363 sig { returns(T::Boolean) }
364 def never_equal!
365 p1 = Coordinate.new(row: 1, col: 2)
366 p2 = Coordinate.new(row: 1, col: 2)
367 p1 != p2
368 end
369
370 # Define your own `#==` method to check the fields, if that's what you want.
371 class Position < T::Struct
372 extend T::Sig
373
374 const :x, Integer
375 const :y, Integer
376
377 sig { params(other: Object).returns(T::Boolean) }
378 def ==(other)
379 # There's a real implementation here:
380 # https://github.com/tricycle/sorbet-struct-comparable
381 true
382 end
383 end
384
385 # Use `T::Enum` to define a fixed set of values that are easy to reference.
386 # This is especially useful when you don't care what the values _are_ as much
387 # as you care that the set of possibilities is closed and static.
388 class Crayon < T::Enum
389 extend T::Sig
390
391 # Initialize members with `enums`.
392 enums do
393 # Define each member with `new`. Each of these is an instance of the
394 # `Crayon` class.
395 Red = new
396 Orange = new
397 Yellow = new
398 Green = new
399 Blue = new
400 Violet = new
401 Brown = new
402 Black = new
403 # The default value of the enum is its name in all-lowercase. To change
404 # that, pass a value to `new`.
405 Gray90 = new('light-gray')
406 end
407
408 sig { returns(String) }
409 def to_hex
410 case self
411 when Red then '#ff0000'
412 when Green then '#00ff00'
413 # ...
414 else '#ffffff'
415 end
416 end
417 end
418
419 sig { params(crayon: Crayon, path: T::Array[Position]).void }
420 def draw(crayon:, path:)
421 path.each do |pos|
422 Kernel.puts "(#{pos.x}, #{pos.y}) = " + crayon.to_hex
423 end
424 end
425
426 # To get all the values in the enum, use `.values`. For convenience there's
427 # already a `#serialize` to get the enum string value.
428
429 sig { returns(T::Array[String]) }
430 def crayon_names
431 Crayon.values.map(&:serialize)
432 end
433
434 # Use the "deserialize" family to go from string to enum value.
435
436 sig { params(name: String).returns(T.nilable(Crayon)) }
437 def crayon_from_name(name)
438 if Crayon.has_serialized?(name)
439 # If the value is not found, this will raise a `KeyError`.
440 Crayon.deserialize(name)
441 end
442
443 # If the value is not found, this will return `nil`.
444 Crayon.try_deserialize(name)
445 end
446end
447
448module FlowSensitivity
449 extend T::Sig
450 # Sorbet understands Ruby's control flow constructs and uses that information
451 # to get more accurate types when your code branches.
452
453 # You'll see this most often when doing nil checks.
454 sig { params(name: T.nilable(String)).returns(String) }
455 def greet_loudly(name)
456 if name.nil?
457 'HELLO, YOU!'
458 else
459 # Sorbet knows that `name` must be a String here, so it's safe to call
460 # `#upcase`.
461 "HELLO, #{name.upcase}!"
462 end
463 end
464
465 # The nils are a special case of refining `T.any`.
466 sig { params(id: T.any(Integer, T::Array[Integer])).returns(T::Array[String]) }
467 def database_lookup(id)
468 if id.is_a?(Integer)
469 # `ids` must be an Integer here.
470 [id.to_s]
471 else
472 # `ids` must be a T::Array[Integer] here.
473 id.map(&:to_s)
474 end
475 end
476
477 # Sorbet recognizes these methods that narrow type definitions:
478 # * is_a?
479 # * kind_of?
480 # * nil?
481 # * Class#===
482 # * Class#<
483 # * block_given?
484 #
485 # Because they're so common, it also recognizes these Rails extensions:
486 # * blank?
487 # * present?
488 #
489 # Be careful to maintain Sorbet assumptions if you redefine these methods!
490
491 # Have you ever written this line of code?
492 #
493 # raise StandardError, "Can't happen"
494 #
495 # Sorbet can help you prove that statically (this is known as
496 # "exhaustiveness") with `T.absurd`. It's extra cool when combined with
497 # `T::Enum`!
498
499 class Size < T::Enum
500 extend T::Sig
501
502 enums do
503 Byte = new('B')
504 Kibibyte = new('KiB')
505 Mebibyte = new('MiB')
506 # "640K ought to be enough for anybody"
507 end
508
509 sig { returns(Integer) }
510 def bytes
511 case self
512 when Byte then 1 << 0
513 when Kibibyte then 1 << 10
514 when Mebibyte then 1 << 20
515 else
516 # Sorbet knows you've checked all the cases, so there's no possible
517 # value that `self` could have here.
518 #
519 # But if you _do_ get here somehow, this will raise at runtime.
520 T.absurd(self)
521
522 # If you're missing a case, Sorbet can even tell you which one it is!
523 end
524 end
525 end
526
527 # We're gonna need `puts` and `raise` for this next part.
528 include Kernel
529
530 # Sorbet knows that no code can execute after a `raise` statement because it
531 # "never returns".
532 sig { params(num: T.nilable(Integer)).returns(Integer) }
533 def decrement(num)
534 raise ArgumentError, '¯\_(ツ)_/¯' unless num
535
536 num - 1
537 end
538
539 class CustomError < StandardError; end
540
541 # You can annotate your own error-raising methods with `T.noreturn`.
542 sig { params(message: String).returns(T.noreturn) }
543 def oh_no(message = 'A bad thing happened')
544 puts message
545 raise CustomError, message
546 end
547
548 # Infinite loops also don't return.
549 sig { returns(T.noreturn) }
550 def loading
551 loop do
552 %q(-\|/).each_char do |c|
553 print "\r#{c} reticulating splines..."
554 sleep 1
555 end
556 end
557 end
558
559 # You may run into a situation where Sorbet "loses" your type refinement.
560 # Remember that almost everything you do in Ruby is a method call that could
561 # return a different value next time you call it. Sorbet doesn't assume that
562 # any methods are pure (even those from `attr_reader` and `attr_accessor`).
563 sig { returns(T.nilable(Integer)) }
564 def answer
565 rand > 0.5 ? 42 : nil
566 end
567
568 sig { void }
569 def bad_typecheck
570 if answer.nil?
571 0
572 else
573 # But answer might return `nil` if we call it again!
574 answer + 1
575 # ^ Method + does not exist on NilClass component of T.nilable(Integer)
576 end
577 end
578
579 sig { void }
580 def good_typecheck
581 ans = answer
582 if ans.nil?
583 0
584 else
585 # This time, Sorbet knows that `ans` is non-nil.
586 ans + 1
587 end
588 end
589end
590
591module InheritancePatterns
592 extend T::Sig
593
594 # If you have a method that always returns the type of its receiver, use
595 # `T.self_type`. This is common in fluent interfaces and DSLs.
596 #
597 # Warning: This feature is still experimental!
598 class Logging
599 extend T::Sig
600
601 sig { returns(T.self_type) }
602 def log
603 pp self
604 self
605 end
606 end
607
608 class Data < Logging
609 extend T::Sig
610
611 sig { params(x: Integer, y: String).void }
612 def initialize(x: 0, y: '')
613 @x = x
614 @y = y
615 end
616
617 # You don't _have_ to use `T.self_type` if there's only one relevant class.
618 sig { params(x: Integer).returns(Data) }
619 def setX(x)
620 @x = x
621 self
622 end
623
624 sig { params(y: String).returns(Data) }
625 def setY(y)
626 @y = y
627 self
628 end
629 end
630
631 # Ta-da!
632 sig { params(data: Data).void }
633 def chaining(data)
634 data.setX(1).log.setY('a')
635 end
636
637 # If it's a class method (a.k.a. singleton method), use `T.attached_class`.
638 #
639 # No warning here. This one is stable!
640 class Box
641 extend T::Sig
642
643 sig { params(contents: String, weight: Integer).void }
644 def initialize(contents, weight)
645 @contents = contents
646 @weight = weight
647 end
648
649 sig { params(contents: String).returns(T.attached_class) }
650 def self.pack(contents)
651 new(contents, contents.chars.uniq.length)
652 end
653 end
654
655 class CompanionCube < Box
656 extend T::Sig
657
658 sig { returns(String) }
659 def pick_up
660 "♥#{@contents}🤍"
661 end
662 end
663
664 sig { returns(String) }
665 def befriend
666 CompanionCube.pack('').pick_up
667 end
668
669 # Sorbet has support for abstract classes and interfaces. It can check that
670 # all the concrete classes and implementations actually define the required
671 # methods with compatible signatures.
672
673 # Here's an abstract class:
674
675 class WorkflowStep
676 extend T::Sig
677
678 # Bring in the inheritance helpers.
679 extend T::Helpers
680
681 # Mark this class as abstract. This means it cannot be instantiated with
682 # `.new`, but it can still be subclassed.
683 abstract!
684
685 sig { params(args: T::Array[String]).void }
686 def run(args)
687 pre_hook
688 execute(args)
689 post_hook
690 end
691
692 # This is an abstract method, which means it _must_ be implemented by
693 # subclasses. Add a signature with `abstract` to an empty method to tell
694 # Sorbet about it.
695 #
696 # If this implementation of the method actually gets called at runtime, it
697 # will raise `NotImplementedError`.
698 sig { abstract.params(args: T::Array[String]).void }
699 def execute(args); end
700
701 # The following non-abstract methods _can_ be implemented by subclasses,
702 # but they're optional.
703
704 sig { void }
705 def pre_hook; end
706
707 sig { void }
708 def post_hook; end
709 end
710
711 class Configure < WorkflowStep
712 extend T::Sig
713
714 sig { void }
715 def pre_hook
716 puts 'Configuring...'
717 end
718
719 # To implement an abstract method, mark the signature with `override`.
720 sig { override.params(args: T::Array[String]).void }
721 def execute(args)
722 # ...
723 end
724 end
725
726 # And here's an interface:
727
728 module Queue
729 extend T::Sig
730
731 # Bring in the inheritance helpers.
732 extend T::Helpers
733
734 # Mark this module as an interface. This adds the following restrictions:
735 # 1. All of its methods must be abstract.
736 # 2. It cannot have any private or protected methods.
737 interface!
738
739 sig { abstract.params(num: Integer).void }
740 def push(num); end
741
742 sig { abstract.returns(T.nilable(Integer)) }
743 def pop; end
744 end
745
746 class PriorityQueue
747 extend T::Sig
748
749 # Include the interface to tell Sorbet that this class implements it.
750 # Sorbet doesn't support implicitly implemented interfaces (also known as
751 # "duck typing").
752 include Queue
753
754 sig { void }
755 def initialize
756 @items = T.let([], T::Array[Integer])
757 end
758
759 # Implement the Queue interface's abstract methods. Remember to use
760 # `override`!
761
762 sig { override.params(num: Integer).void }
763 def push(num)
764 @items << num
765 @items.sort!
766 end
767
768 sig { override.returns(T.nilable(Integer)) }
769 def pop
770 @items.shift
771 end
772 end
773
774 # If you use the `included` hook to get class methods from your modules,
775 # you'll have to use `mixes_in_class_methods` to get them to type-check.
776
777 module Mixin
778 extend T::Helpers
779 interface!
780
781 module ClassMethods
782 extend T::Sig
783
784 sig { void }
785 def whisk
786 'fskfskfsk'
787 end
788 end
789
790 mixes_in_class_methods(ClassMethods)
791 end
792
793 class EggBeater
794 include Mixin
795 end
796
797 EggBeater.whisk # Meringue!
798end
799
800module EscapeHatches
801 extend T::Sig
802
803 # Ruby is a very dynamic language, and sometimes Sorbet can't infer the
804 # properties you already know to be true. Although there are ways to rewrite
805 # your code so Sorbet can prove safety, you can also choose to "break out" of
806 # Sorbet using these "escape hatches".
807
808 # Once you start using `T.nilable`, Sorbet will start telling you _all_ the
809 # places you're not handling nils. Sometimes, you know a value can't be nil,
810 # but it's not practical to fix the sigs so Sorbet can prove it. In that
811 # case, you can use `T.must`.
812 sig { params(maybe_str: T.nilable(String)).returns(String) }
813 def no_nils_here(maybe_str)
814 # If maybe_str _is_ actually nil, this will error at runtime.
815 str = T.must(maybe_str)
816 str.downcase
817 end
818
819 # More generally, if you know that a value must be a specific type, you can
820 # use `T.cast`.
821 sig do
822 params(
823 str_or_ary: T.any(String, T::Array[String]),
824 idx_or_range: T.any(Integer, T::Range[Integer]),
825 ).returns(T::Array[String])
826 end
827 def slice2(str_or_ary, idx_or_range)
828 # Let's say that, for some reason, we want individual characters from
829 # strings or sub-arrays from arrays. The other options are not allowed.
830 if str_or_ary.is_a?(String)
831 # Here, we know that `idx_or_range` must be a single index. If it's not,
832 # this will error at runtime.
833 idx = T.cast(idx_or_range, Integer)
834 [str_or_ary.chars.fetch(idx)]
835 else
836 # Here, we know that `idx_or_range` must be a range. If it's not, this
837 # will error at runtime.
838 range = T.cast(idx_or_range, T::Range[Integer])
839 str_or_ary.slice(range) || []
840 end
841 end
842
843 # If you know that a method exists, but Sorbet doesn't, you can use
844 # `T.unsafe` so Sorbet will let you call it. Although we tend to think of
845 # this as being an "unsafe method call", `T.unsafe` is called on the receiver
846 # rather than the whole expression.
847 sig { params(count: Integer).returns(Date) }
848 def the_future(count)
849 # Let's say you've defined some extra date helpers that Sorbet can't find.
850 # So `2.decades` is effectively `(2*10).years` from ActiveSupport.
851 Date.today + T.unsafe(count).decades
852 end
853
854 # If this is a method on the implicit `self`, you'll have to make that
855 # explicit to use `T.unsafe`.
856 sig { params(count: Integer).returns(Date) }
857 def the_past(count)
858 # Let's say that metaprogramming defines a `now` helper method for
859 # `Time.new`. Using it would normally look like this:
860 #
861 # now - 1234
862 #
863 T.unsafe(self).now - 1234
864 end
865
866 # There's a special type in Sorbet called `T.untyped`. For any value of this
867 # type, Sorbet will allow it to be used for any method argument and receive
868 # any method call.
869
870 sig { params(num: Integer, anything: T.untyped).returns(T.untyped) }
871 def nothing_to_see_here(num, anything)
872 anything.digits # Is it an Integer...
873 anything.upcase # ... or a String?
874
875 # Sorbet will not be able to infer anything about this return value because
876 # it's untyped.
877 BasicObject.new
878 end
879
880 def see_here
881 # It's actually nil! This will crash at runtime, but Sorbet allows it.
882 nothing_to_see_here(1, nil)
883 end
884
885 # For a method without a sig, Sorbet infers the type of each argument and the
886 # return value to be `T.untyped`.
887end
888
889# The following types are not officially documented but are still useful. They
890# may be experimental, deprecated, or not supported.
891
892module ValueSet
893 extend T::Sig
894
895 # A common pattern in Ruby is to have a method accept one value from a set of
896 # options. Especially when starting out with Sorbet, it may not be practical
897 # to refactor the code to use `T::Enum`. In this case, you can use `T.enum`.
898 #
899 # Note: Sorbet can't check this statically because it doesn't track the
900 # values themselves.
901 sig do
902 params(
903 data: T::Array[Numeric],
904 shape: T.enum([:circle, :square, :triangle])
905 ).void
906 end
907 def plot_points(data, shape: :circle)
908 data.each_with_index do |y, x|
909 Kernel.puts "#{x}: #{y}"
910 end
911 end
912end
913
914module Generics
915 extend T::Sig
916
917 # Generics are useful when you have a class whose method types change based
918 # on the data it contains or a method whose method type changes based on what
919 # its arguments are.
920
921 # A generic method uses `type_parameters` to declare type variables and
922 # `T.type_parameter` to refer back to them.
923 sig do
924 type_parameters(:element)
925 .params(
926 element: T.type_parameter(:element),
927 count: Integer,
928 ).returns(T::Array[T.type_parameter(:element)])
929 end
930 def repeat_value(element, count)
931 count.times.each_with_object([]) do |elt, ary|
932 ary << elt
933 end
934 end
935
936 sig do
937 type_parameters(:element)
938 .params(
939 count: Integer,
940 block: T.proc.returns(T.type_parameter(:element)),
941 ).returns(T::Array[T.type_parameter(:element)])
942 end
943 def repeat_cached(count, &block)
944 elt = block.call
945 ary = []
946 count.times do
947 ary << elt
948 end
949 ary
950 end
951
952 # A generic class uses `T::Generic.type_member` to define type variables that
953 # can be like regular type names.
954 class BidirectionalHash
955 extend T::Sig
956 extend T::Generic
957
958 Left = type_member
959 Right = type_member
960
961 sig { void }
962 def initialize
963 @left_hash = T.let({}, T::Hash[Left, Right])
964 @right_hash = T.let({}, T::Hash[Right, Left])
965 end
966
967 # Implement just enough to make the methods below work.
968
969 sig { params(lkey: Left).returns(T::Boolean) }
970 def lhas?(lkey)
971 @left_hash.has_key?(lkey)
972 end
973
974 sig { params(rkey: Right).returns(T.nilable(Left)) }
975 def rget(rkey)
976 @right_hash[rkey]
977 end
978 end
979
980 # To specialize a generic type, use brackets.
981 sig do
982 params(
983 options: BidirectionalHash[Symbol, Integer],
984 choice: T.any(Symbol, Integer),
985 ).returns(T.nilable(String))
986 end
987 def lookup(options, choice)
988 case choice
989 when Symbol
990 options.lhas?(choice) ? choice.to_s : nil
991 when Integer
992 options.rget(choice).to_s
993 else
994 T.absurd(choice)
995 end
996 end
997
998 # To specialize through inheritance, re-declare the `type_member` with
999 # `fixed`.
1000 class Options < BidirectionalHash
1001 Left = type_member(fixed: Symbol)
1002 Right = type_member(fixed: Integer)
1003 end
1004
1005 sig do
1006 params(
1007 options: Options,
1008 choice: T.any(Symbol, Integer),
1009 ).returns(T.nilable(String))
1010 end
1011 def lookup2(options, choice)
1012 lookup(options, choice)
1013 end
1014
1015 # There are other variance annotations you can add to `type_member`, but
1016 # they're rarely used.
1017end
Additional resources ¶
- Official Documentation
- sorbet.run - Playground