Sorbet Cheatsheet Part 4
Published on
Note: This was an incomplete draft. You probably want the finished version!
I'm watching Fleet Fatales 2020 on GamesDoneQuick! Filling out the Sorbet cheatsheet is a great thing to do between runs 😄
Changes from last time:
- Fill in the combinators section.
- Use modules instead of classes to group sections.
# 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.
# 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) }
end
# Define parameter value types with `params`.
sig { params(n: Integer).returns(String) }
(1..n).map { greet }.join()
end
# Define keyword parameters the same way.
sig { params(n: Integer, sep: String).returns(String) }
(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
end
# For a method whose return value is useless, use `void`.
sig { params(name: String).void }
puts
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 }
if kwargs[:op] ==
args.each { puts(i - 1) }
else
args.each { puts(i + 1) }
end
end
# Most initializers should be `void`.
sig { params(name: String).void }
# 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.
= T.let(, String)
# Class variables too.
@@the_answer = T.let(42, Integer)
end
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) }
str ==
end
# Reminder that the value `nil` is an instance of NilClass.
sig { params(val: NilClass).void }
; 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]) }
keyset = [:old_key, :new_key]
config.each_pair.flat_map do
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).
; end
sig { params(dep: T.class_of(Dep)).returns(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
data.sum(&blk)
end
sig { returns(Integer) }
count([, , ]) { 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) }
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 }
num.instance_eval(&blk)
end
sig { void }
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) }
2 * blk.call
end
end
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(Integer) }
num.round(-2)
end
# `T.nilable(Type)` is a convenient alias for `T.any(Type, NilClass)`.
sig { params(val: T.nilable(String)).returns(Integer) }
val.nil? ? -1 : val.length
end
# Use `T.all` when you have a value that must be satisfy multiple types.
# These are sometimes known as "intersection types". They're most useful for
# interfaces (described later), but can also be useful for helper modules.
extend T::Sig
sig { void }
# Pretend this is actually implemented
end
end
extend T::Sig
sig { void }
# Pretend this is actually implemented
end
end
include Reversible
include Sortable
end
sig { params(list: T.all(Reversible, Sortable)).void }
# reverse from Reversible
list.reverse
# sort from Sortable
list.sort
end
rev_sort(List.new)
end
end
# TODO: T::Struct
# These are sometimes known as "product types".
# TODO: ^ reference equality, not value equality
# TODO: T::Enum
end
# TODO: T.absurd
# TODO: T.noreturn
end
# TODO: T.type_alias
# TODO: T.self_type
# TODO: T.attached_class
end
# TODO: abstract!
# TODO: interface!
# TODO: abstract. / override.
# TODO: mixes_in_class_methods
# TODO: final!
# TODO: sealed!
end
# TODO: T.reveal_type
end
# 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.
# TODO: T.enum
end
# TODO: type_parameters / T.type_parameter
# TODO: T::Generic
# TODO: type_member
end