summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--sorbet.html.markdown1040
1 files changed, 1040 insertions, 0 deletions
diff --git a/sorbet.html.markdown b/sorbet.html.markdown
new file mode 100644
index 00000000..9c2e809b
--- /dev/null
+++ b/sorbet.html.markdown
@@ -0,0 +1,1040 @@
+---
+language: sorbet
+filename: learnsorbet.rb
+contributors:
+ - ["Jeremy Kaplan", "https://jdkaplan.dev"]
+---
+
+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](https://sorbet.run).
+
+Try copying in one of the sections below! Each top-level `class` or `module`
+is independent from the others.
+
+```ruby
+# 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_2(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)
+
+ # Sorbet knows about the `attr_*` family.
+ sig { returns(String) }
+ attr_reader :upname
+
+ sig { params(write_only: Integer).returns(Integer) }
+ attr_writer :write_only
+
+ # You say the reader part and Sorbet will say the writer part.
+ sig { returns(String) }
+ attr_accessor :name
+end
+
+module Debugging
+ extend T::Sig
+
+ # Sometimes it's helpful to know what type Sorbet has inferred for an
+ # expression. Use `T.reveal_type` to make type-checking show a special error
+ # with that information.
+ #
+ # This is most useful if you have Sorbet integrated into your editor so you
+ # can see the result as soon as you save the file.
+
+ sig { params(obj: Object).returns(String) }
+ def debug(obj)
+ T.reveal_type(obj) # Revealed type: Object
+ repr = obj.inspect
+
+ # Remember that Ruby methods can be called without arguments, so you can
+ # save a couple characters!
+ T.reveal_type repr # Revealed type: String
+
+ "DEBUG: " + repr
+ end
+end
+
+module StandardLibrary
+ extend T::Sig
+ # 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
+
+ # Remember that the value `nil` is an instance of NilClass.
+ sig { params(val: NilClass).void }
+ def only_nil(val:); end
+
+ # To avoid modifying 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 : 'sensible default'
+ 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 { params(num: Integer).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
+
+module Combinators
+ extend T::Sig
+ # These methods let you define new types from existing types.
+
+ # Use `T.any` when you have a value that can be one of many types. These are
+ # sometimes known as "union types" or "sum types".
+ sig { params(num: T.any(Integer, Float)).returns(Rational) }
+ def hundreds(num)
+ num.rationalize
+ end
+
+ # `T.nilable(Type)` is a convenient alias for `T.any(Type, NilClass)`.
+ sig { params(val: T.nilable(String)).returns(Integer) }
+ def strlen(val)
+ val.nil? ? -1 : val.length
+ end
+
+ # Use `T.all` when you have a value that must satisfy multiple types. These
+ # are sometimes known as "intersection types". They're most useful for
+ # interfaces (described later), but can also describe helper modules.
+
+ module Reversible
+ extend T::Sig
+ sig { void }
+ def reverse
+ # Pretend this is actually implemented
+ end
+ end
+
+ module Sortable
+ extend T::Sig
+ sig { void }
+ def sort
+ # Pretend this is actually implemented
+ end
+ end
+
+ class List
+ include Reversible
+ include Sortable
+ end
+
+ sig { params(list: T.all(Reversible, Sortable)).void }
+ def rev_sort(list)
+ # reverse from Reversible
+ list.reverse
+ # sort from Sortable
+ list.sort
+ end
+
+ def rev_sort_usage
+ rev_sort(List.new)
+ end
+
+ # Sometimes, actually spelling out the type every time becomes more confusing
+ # than helpful. Use type aliases to make them easier to work with.
+ JSONLiteral = T.type_alias { T.any(Float, String, T::Boolean, NilClass) }
+
+ sig { params(val: JSONLiteral).returns(String) }
+ def stringify(val)
+ val.to_s
+ end
+end
+
+module DataClasses
+ extend T::Sig
+ # Use `T::Struct` to create a new class with type-checked fields. It combines
+ # the best parts of the standard Struct and OpenStruct, and then adds static
+ # typing on top.
+ #
+ # Types constructed this way are sometimes known as "product types".
+
+ class Matcher < T::Struct
+ # Use `prop` to define a field with both a reader and writer.
+ prop :count, Integer
+ # Use `const` to only define the reader and skip the writer.
+ const :pattern, Regexp
+ # You can still set a default value with `default`.
+ const :message, String, default: 'Found one!'
+
+ # This is otherwise a normal class, so you can still define methods.
+
+ # You'll still need to bring `sig` in if you want to use it though.
+ extend T::Sig
+
+ sig { void }
+ def reset
+ self.count = 0
+ end
+ end
+
+ sig { params(text: String, matchers: T::Array[Matcher]).void }
+ def awk(text, matchers)
+ matchers.each(&:reset)
+ text.lines.each do |line|
+ matchers.each do |matcher|
+ if matcher.pattern =~ line
+ Kernel.puts matcher.message
+ matcher.count += 1
+ end
+ end
+ end
+ end
+
+ # Gotchas and limitations
+
+ # 1. `const` fields are not truly immutable. They don't have a writer method,
+ # but may be changed in other ways.
+ class ChangeMe < T::Struct
+ const :list, T::Array[Integer]
+ end
+
+ sig { params(change_me: ChangeMe).returns(T::Boolean) }
+ def whoops!(change_me)
+ change_me = ChangeMe.new(list: [1, 2, 3, 4])
+ change_me.list.reverse!
+ change_me.list == [4, 3, 2, 1]
+ end
+
+ # 2. `T::Struct` inherits its equality method from `BasicObject`, which uses
+ # identity equality (also known as "reference equality").
+ class Coordinate < T::Struct
+ const :row, Integer
+ const :col, Integer
+ end
+
+ sig { returns(T::Boolean) }
+ def never_equal!
+ p1 = Coordinate.new(row: 1, col: 2)
+ p2 = Coordinate.new(row: 1, col: 2)
+ p1 != p2
+ end
+
+ # Define your own `#==` method to check the fields, if that's what you want.
+ class Position < T::Struct
+ extend T::Sig
+
+ const :x, Integer
+ const :y, Integer
+
+ sig { params(other: Object).returns(T::Boolean) }
+ def ==(other)
+ # There's a real implementation here:
+ # https://github.com/tricycle/sorbet-struct-comparable
+ true
+ end
+ end
+
+ # Use `T::Enum` to define a fixed set of values that are easy to reference.
+ # This is especially useful when you don't care what the values _are_ as much
+ # as you care that the set of possibilities is closed and static.
+ class Crayon < T::Enum
+ extend T::Sig
+
+ # Initialize members with `enums`.
+ enums do
+ # Define each member with `new`. Each of these is an instance of the
+ # `Crayon` class.
+ Red = new
+ Orange = new
+ Yellow = new
+ Green = new
+ Blue = new
+ Violet = new
+ Brown = new
+ Black = new
+ # The default value of the enum is its name in all-lowercase. To change
+ # that, pass a value to `new`.
+ Gray90 = new('light-gray')
+ end
+
+ sig { returns(String) }
+ def to_hex
+ case self
+ when Red then '#ff0000'
+ when Green then '#00ff00'
+ # ...
+ else '#ffffff'
+ end
+ end
+ end
+
+ sig { params(crayon: Crayon, path: T::Array[Position]).void }
+ def draw(crayon:, path:)
+ path.each do |pos|
+ Kernel.puts "(#{pos.x}, #{pos.y}) = " + crayon.to_hex
+ end
+ end
+
+ # To get all the values in the enum, use `.values`. For convenience there's
+ # already a `#serialize` to get the enum string value.
+
+ sig { returns(T::Array[String]) }
+ def crayon_names
+ Crayon.values.map(&:serialize)
+ end
+
+ # Use the "deserialize" family to go from string to enum value.
+
+ sig { params(name: String).returns(T.nilable(Crayon)) }
+ def crayon_from_name(name)
+ if Crayon.has_serialized?(name)
+ # If the value is not found, this will raise a `KeyError`.
+ Crayon.deserialize(name)
+ end
+
+ # If the value is not found, this will return `nil`.
+ Crayon.try_deserialize(name)
+ end
+end
+
+module FlowSensitivity
+ extend T::Sig
+ # Sorbet understands Ruby's control flow constructs and uses that information
+ # to get more accurate types when your code branches.
+
+ # You'll see this most often when doing nil checks.
+ sig { params(name: T.nilable(String)).returns(String) }
+ def greet_loudly(name)
+ if name.nil?
+ 'HELLO, YOU!'
+ else
+ # Sorbet knows that `name` must be a String here, so it's safe to call
+ # `#upcase`.
+ "HELLO, #{name.upcase}!"
+ end
+ end
+
+ # The nils are a special case of refining `T.any`.
+ sig { params(id: T.any(Integer, T::Array[Integer])).returns(T::Array[String]) }
+ def database_lookup(id)
+ if id.is_a?(Integer)
+ # `ids` must be an Integer here.
+ [id.to_s]
+ else
+ # `ids` must be a T::Array[Integer] here.
+ id.map(&:to_s)
+ end
+ end
+
+ # Sorbet recognizes these methods that narrow type definitions:
+ # * is_a?
+ # * kind_of?
+ # * nil?
+ # * Class#===
+ # * Class#<
+ # * block_given?
+ #
+ # Because they're so common, it also recognizes these Rails extensions:
+ # * blank?
+ # * present?
+ #
+ # Be careful to maintain Sorbet assumptions if you redefine these methods!
+
+ # Have you ever written this line of code?
+ #
+ # raise StandardError, "Can't happen"
+ #
+ # Sorbet can help you prove that statically (this is known as
+ # "exhaustiveness") with `T.absurd`. It's extra cool when combined with
+ # `T::Enum`!
+
+ class Size < T::Enum
+ extend T::Sig
+
+ enums do
+ Byte = new('B')
+ Kibibyte = new('KiB')
+ Mebibyte = new('MiB')
+ # "640K ought to be enough for anybody"
+ end
+
+ sig { returns(Integer) }
+ def bytes
+ case self
+ when Byte then 1 << 0
+ when Kibibyte then 1 << 10
+ when Mebibyte then 1 << 20
+ else
+ # Sorbet knows you've checked all the cases, so there's no possible
+ # value that `self` could have here.
+ #
+ # But if you _do_ get here somehow, this will raise at runtime.
+ T.absurd(self)
+
+ # If you're missing a case, Sorbet can even tell you which one it is!
+ end
+ end
+ end
+
+ # We're gonna need `puts` and `raise` for this next part.
+ include Kernel
+
+ # Sorbet knows that no code can execute after a `raise` statement because it
+ # "never returns".
+ sig { params(num: T.nilable(Integer)).returns(Integer) }
+ def decrement(num)
+ raise ArgumentError, '¯\_(ツ)_/¯' unless num
+
+ num - 1
+ end
+
+ class CustomError < StandardError; end
+
+ # You can annotate your own error-raising methods with `T.noreturn`.
+ sig { params(message: String).returns(T.noreturn) }
+ def oh_no(message = 'A bad thing happened')
+ puts message
+ raise CustomError, message
+ end
+
+ # Infinite loops also don't return.
+ sig { returns(T.noreturn) }
+ def loading
+ loop do
+ %q(-\|/).each_char do |c|
+ print "\r#{c} reticulating splines..."
+ sleep 1
+ end
+ end
+ end
+
+ # You may run into a situation where Sorbet "loses" your type refinement.
+ # Remember that almost everything you do in Ruby is a method call that could
+ # return a different value next time you call it. Sorbet doesn't assume that
+ # any methods are pure (even those from `attr_reader` and `attr_accessor`).
+ sig { returns(T.nilable(Integer)) }
+ def answer
+ rand > 0.5 ? 42 : nil
+ end
+
+ sig { void }
+ def bad_typecheck
+ if answer.nil?
+ 0
+ else
+ # But answer might return `nil` if we call it again!
+ answer + 1
+ # ^ Method + does not exist on NilClass component of T.nilable(Integer)
+ end
+ end
+
+ sig { void }
+ def good_typecheck
+ ans = answer
+ if ans.nil?
+ 0
+ else
+ # This time, Sorbet knows that `ans` is non-nil.
+ ans + 1
+ end
+ end
+end
+
+module InheritancePatterns
+ extend T::Sig
+
+ # If you have a method that always returns the type of its receiver, use
+ # `T.self_type`. This is common in fluent interfaces and DSLs.
+ #
+ # Warning: This feature is still experimental!
+ class Logging
+ extend T::Sig
+
+ sig { returns(T.self_type) }
+ def log
+ pp self
+ self
+ end
+ end
+
+ class Data < Logging
+ extend T::Sig
+
+ sig { params(x: Integer, y: String).void }
+ def initialize(x: 0, y: '')
+ @x = x
+ @y = y
+ end
+
+ # You don't _have_ to use `T.self_type` if there's only one relevant class.
+ sig { params(x: Integer).returns(Data) }
+ def setX(x)
+ @x = x
+ self
+ end
+
+ sig { params(y: String).returns(Data) }
+ def setY(y)
+ @y = y
+ self
+ end
+ end
+
+ # Ta-da!
+ sig { params(data: Data).void }
+ def chaining(data)
+ data.setX(1).log.setY('a')
+ end
+
+ # If it's a class method (a.k.a. singleton method), use `T.attached_class`.
+ #
+ # No warning here. This one is stable!
+ class Box
+ extend T::Sig
+
+ sig { params(contents: String, weight: Integer).void }
+ def initialize(contents, weight)
+ @contents = contents
+ @weight = weight
+ end
+
+ sig { params(contents: String).returns(T.attached_class) }
+ def self.pack(contents)
+ new(contents, contents.chars.uniq.length)
+ end
+ end
+
+ class CompanionCube < Box
+ extend T::Sig
+
+ sig { returns(String) }
+ def pick_up
+ "♥#{@contents}🤍"
+ end
+ end
+
+ sig { returns(String) }
+ def befriend
+ CompanionCube.pack('').pick_up
+ end
+
+ # Sorbet has support for abstract classes and interfaces. It can check that
+ # all the concrete classes and implementations actually define the required
+ # methods with compatible signatures.
+
+ # Here's an abstract class:
+
+ class WorkflowStep
+ extend T::Sig
+
+ # Bring in the inheritance helpers.
+ extend T::Helpers
+
+ # Mark this class as abstract. This means it cannot be instantiated with
+ # `.new`, but it can still be subclassed.
+ abstract!
+
+ sig { params(args: T::Array[String]).void }
+ def run(args)
+ pre_hook
+ execute(args)
+ post_hook
+ end
+
+ # This is an abstract method, which means it _must_ be implemented by
+ # subclasses. Add a signature with `abstract` to an empty method to tell
+ # Sorbet about it.
+ #
+ # If this implementation of the method actually gets called at runtime, it
+ # will raise `NotImplementedError`.
+ sig { abstract.params(args: T::Array[String]).void }
+ def execute(args); end
+
+ # The following non-abstract methods _can_ be implemented by subclasses,
+ # but they're optional.
+
+ sig { void }
+ def pre_hook; end
+
+ sig { void }
+ def post_hook; end
+ end
+
+ class Configure < WorkflowStep
+ extend T::Sig
+
+ sig { void }
+ def pre_hook
+ puts 'Configuring...'
+ end
+
+ # To implement an abstract method, mark the signature with `override`.
+ sig { override.params(args: T::Array[String]).void }
+ def execute(args)
+ # ...
+ end
+ end
+
+ # And here's an interface:
+
+ module Queue
+ extend T::Sig
+
+ # Bring in the inheritance helpers.
+ extend T::Helpers
+
+ # Mark this module as an interface. This adds the following restrictions:
+ # 1. All of its methods must be abstract.
+ # 2. It cannot have any private or protected methods.
+ interface!
+
+ sig { abstract.params(num: Integer).void }
+ def push(num); end
+
+ sig { abstract.returns(T.nilable(Integer)) }
+ def pop; end
+ end
+
+ class PriorityQueue
+ extend T::Sig
+
+ # Include the interface to tell Sorbet that this class implements it.
+ # Sorbet doesn't support implicitly implemented interfaces (also known as
+ # "duck typing").
+ include Queue
+
+ sig { void }
+ def initialize
+ @items = T.let([], T::Array[Integer])
+ end
+
+ # Implement the Queue interface's abstract methods. Remember to use
+ # `override`!
+
+ sig { override.params(num: Integer).void }
+ def push(num)
+ @items << num
+ @items.sort!
+ end
+
+ sig { override.returns(T.nilable(Integer)) }
+ def pop
+ @items.shift
+ end
+ end
+
+ # If you use the `included` hook to get class methods from your modules,
+ # you'll have to use `mixes_in_class_methods` to get them to type-check.
+
+ module Mixin
+ extend T::Helpers
+ interface!
+
+ module ClassMethods
+ extend T::Sig
+
+ sig { void }
+ def whisk
+ 'fskfskfsk'
+ end
+ end
+
+ mixes_in_class_methods(ClassMethods)
+ end
+
+ class EggBeater
+ include Mixin
+ end
+
+ EggBeater.whisk # Meringue!
+end
+
+module EscapeHatches
+ extend T::Sig
+
+ # Ruby is a very dynamic language, and sometimes Sorbet can't infer the
+ # properties you already know to be true. Although there are ways to rewrite
+ # your code so Sorbet can prove safety, you can also choose to "break out" of
+ # Sorbet using these "escape hatches".
+
+ # Once you start using `T.nilable`, Sorbet will start telling you _all_ the
+ # places you're not handling nils. Sometimes, you know a value can't be nil,
+ # but it's not practical to fix the sigs so Sorbet can prove it. In that
+ # case, you can use `T.must`.
+ sig { params(maybe_str: T.nilable(String)).returns(String) }
+ def no_nils_here(maybe_str)
+ # If maybe_str _is_ actually nil, this will error at runtime.
+ str = T.must(maybe_str)
+ str.downcase
+ end
+
+ # More generally, if you know that a value must be a specific type, you can
+ # use `T.cast`.
+ sig do
+ params(
+ str_or_ary: T.any(String, T::Array[String]),
+ idx_or_range: T.any(Integer, T::Range[Integer]),
+ ).returns(T::Array[String])
+ end
+ def slice2(str_or_ary, idx_or_range)
+ # Let's say that, for some reason, we want individual characters from
+ # strings or sub-arrays from arrays. The other options are not allowed.
+ if str_or_ary.is_a?(String)
+ # Here, we know that `idx_or_range` must be a single index. If it's not,
+ # this will error at runtime.
+ idx = T.cast(idx_or_range, Integer)
+ [str_or_ary.chars.fetch(idx)]
+ else
+ # Here, we know that `idx_or_range` must be a range. If it's not, this
+ # will error at runtime.
+ range = T.cast(idx_or_range, T::Range[Integer])
+ str_or_ary.slice(range) || []
+ end
+ end
+
+ # If you know that a method exists, but Sorbet doesn't, you can use
+ # `T.unsafe` so Sorbet will let you call it. Although we tend to think of
+ # this as being an "unsafe method call", `T.unsafe` is called on the receiver
+ # rather than the whole expression.
+ sig { params(count: Integer).returns(Date) }
+ def the_future(count)
+ # Let's say you've defined some extra date helpers that Sorbet can't find.
+ # So `2.decades` is effectively `(2*10).years` from ActiveSupport.
+ Date.today + T.unsafe(count).decades
+ end
+
+ # If this is a method on the implicit `self`, you'll have to make that
+ # explicit to use `T.unsafe`.
+ sig { params(count: Integer).returns(Date) }
+ def the_past(count)
+ # Let's say that metaprogramming defines a `now` helper method for
+ # `Time.new`. Using it would normally look like this:
+ #
+ # now - 1234
+ #
+ T.unsafe(self).now - 1234
+ end
+
+ # There's a special type in Sorbet called `T.untyped`. For any value of this
+ # type, Sorbet will allow it to be used for any method argument and receive
+ # any method call.
+
+ sig { params(num: Integer, anything: T.untyped).returns(T.untyped) }
+ def nothing_to_see_here(num, anything)
+ anything.digits # Is it an Integer...
+ anything.upcase # ... or a String?
+
+ # Sorbet will not be able to infer anything about this return value because
+ # it's untyped.
+ BasicObject.new
+ end
+
+ def see_here
+ # It's actually nil! This will crash at runtime, but Sorbet allows it.
+ nothing_to_see_here(1, nil)
+ end
+
+ # For a method without a sig, Sorbet infers the type of each argument and the
+ # return value to be `T.untyped`.
+end
+
+# The following types are not officially documented but are still useful. They
+# may be experimental, deprecated, or not supported.
+
+module ValueSet
+ extend T::Sig
+
+ # A common pattern in Ruby is to have a method accept one value from a set of
+ # options. Especially when starting out with Sorbet, it may not be practical
+ # to refactor the code to use `T::Enum`. In this case, you can use `T.enum`.
+ #
+ # Note: Sorbet can't check this statically becuase it doesn't track the
+ # values themselves.
+ sig do
+ params(
+ data: T::Array[Numeric],
+ shape: T.enum([:circle, :square, :triangle])
+ ).void
+ end
+ def plot_points(data, shape: :circle)
+ data.each_with_index do |y, x|
+ Kernel.puts "#{x}: #{y}"
+ end
+ end
+end
+
+module Generics
+ extend T::Sig
+
+ # Generics are useful when you have a class whose method types change based
+ # on the data it contains or a method whose method type changes based on what
+ # its arguments are.
+
+ # A generic method uses `type_parameters` to declare type variables and
+ # `T.type_parameter` to refer back to them.
+ sig do
+ type_parameters(:element)
+ .params(
+ element: T.type_parameter(:element),
+ count: Integer,
+ ).returns(T::Array[T.type_parameter(:element)])
+ end
+ def repeat_value(element, count)
+ count.times.each_with_object([]) do |elt, ary|
+ ary << elt
+ end
+ end
+
+ sig do
+ type_parameters(:element)
+ .params(
+ count: Integer,
+ block: T.proc.returns(T.type_parameter(:element)),
+ ).returns(T::Array[T.type_parameter(:element)])
+ end
+ def repeat_cached(count, &block)
+ elt = block.call
+ ary = []
+ count.times do
+ ary << elt
+ end
+ ary
+ end
+
+ # A generic class uses `T::Generic.type_member` to define type variables that
+ # can be like regular type names.
+ class BidirectionalHash
+ extend T::Sig
+ extend T::Generic
+
+ Left = type_member
+ Right = type_member
+
+ sig { void }
+ def initialize
+ @left_hash = T.let({}, T::Hash[Left, Right])
+ @right_hash = T.let({}, T::Hash[Right, Left])
+ end
+
+ # Implement just enough to make the methods below work.
+
+ sig { params(lkey: Left).returns(T::Boolean) }
+ def lhas?(lkey)
+ @left_hash.has_key?(lkey)
+ end
+
+ sig { params(rkey: Right).returns(T.nilable(Left)) }
+ def rget(rkey)
+ @right_hash[rkey]
+ end
+ end
+
+ # To specialize a generic type, use brackets.
+ sig do
+ params(
+ options: BidirectionalHash[Symbol, Integer],
+ choice: T.any(Symbol, Integer),
+ ).returns(T.nilable(String))
+ end
+ def lookup(options, choice)
+ case choice
+ when Symbol
+ options.lhas?(choice) ? choice.to_s : nil
+ when Integer
+ options.rget(choice).to_s
+ else
+ T.absurd(choice)
+ end
+ end
+
+ # To specialize through inheritance, re-declare the `type_member` with
+ # `fixed`.
+ class Options < BidirectionalHash
+ Left = type_member(fixed: Symbol)
+ Right = type_member(fixed: Integer)
+ end
+
+ sig do
+ params(
+ options: Options,
+ choice: T.any(Symbol, Integer),
+ ).returns(T.nilable(String))
+ end
+ def lookup2(options, choice)
+ lookup(options, choice)
+ end
+
+ # There are other variance annotations you can add to `type_member`, but
+ # they're rarely used.
+end
+```
+
+## Additional resources
+
+- [Official Documentation](https://sorbet.org/docs/overview)
+- [sorbet.run](https://sorbet.run) - Playground