Sorbet

Личный сайт Go-разработчика из Казани

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