Sorbet Cheatsheet Part 2

Published on

Note: This was an incomplete draft. You probably want the finished version!

Time to spend another 15 minutes collecting some Sorbet examples! The earlier draft can be found in another Thinkin' Log.

There are no new examples in this one, but the to-dos are more complete and better-organized. I think this includes all the types that I've actually used in my beginner-to-intermediate experience.

# 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
  # TODO: T::Boolean, T::Array, T::Range, T::Hash, T::Enumerable, ...
  # TODO: T.nilable
  # TODO: T.class_of
  # TODO: T.proc
end

class Combinators
  # TODO: T.any
  # TODO: T.all
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