id | title |
---|---|
type-annotations |
Type Annotations |
Sorbet provides the most value when it has a wealth of programmer-supplied static types. However, because Sorbet implements a gradual type system, it treats most definitions without explicit annotations as untyped. This means that Sorbet can only use static types for methods, constants, instance variables, and class variables if they are accompanied with explicit static types.
The exception to the above list is that Sorbet does not need type annotations for local variables. In the absence of a type annotation, Sorbet will infer the type of a local variable based on how the variable is initialized. It is still possible to provide type annotations for local variables.
Type annotations for methods are provided using a sig
before the method
definition. Method type annotations are
described in great detail on the Method Signatures. Other type
annotations are provided using the T.let
type assertion.
Sorbet does not, by default, infer the types of constants, but they can be
specified using T.let
:
NAMES = T.let(["Nelson", "Dmitry", "Paul"], T::Array[String])
To declare the static type of an instance variable, we can use T.let
in a
class's constructor:
class MyObj
def initialize
@foo = T.let(0, Integer)
end
end
We can also declare class variables and instance variables on a singleton class
using T.let
at the top-level of a class:
class HasVariables
# Class variable
@@llamas = T.let([], T::Array[Llama])
# Instance variable on the singleton class
@alpaca_count = T.let(0, Integer)
end
Sorbet requires that instance and class variables are defined in these specific places to guarantee that they're initialized. But sometimes requiring that these variables be declared in specific places is too restrictive. Sorbet allows an instance variable to be declared anywhere so long as the type is at least nilable:
class A
def foo
# Does NOT have to be declared in `initialize`, because it's nilable:
@x = T.let(0, T.nilable(Integer))
end
def self.bar
# Also works for `self.` methods:
@y = T.let('', T.nilable(String))
end
end
It's common to use this technique to add type annotations for instance variables in functions that memoize their results by lazily initializing instance variables:
module B
sig {returns(String)}
def current_user
@user = T.let(@user, T.nilable(String))
@user ||= ENV.fetch('USER')
end
end
A current shortcoming of Sorbet is it cannot reuse static type knowledge in
order to automatically determine the type of an instance or class variable. In
the following example, despite the fact that Sorbet knows that x
has type
Integer
, it still treats @x
as T.untyped
without an explicit type
annotation:
class Foo
sig {params(x: Integer, y: Integer).void}
def initialize(x, y)
@x = x
@y = T.let(y, Integer)
T.reveal_type(@x) # T.untyped
T.reveal_type(@y) # Integer
end
end
This is a known limitation of Sorbet, and we're considering ways to make it less verbose in the future.
Sorbet allows the programmer to opt-in to greater levels of static type rigor.
At lower strictness modes, Sorbet allows definitions to be untyped,
but at # typed: strict
, Sorbet requires explicit type annotations on any
definitions where it would have assumed T.untyped
without an annotation
before. Specifically, in a # typed: strict
file it's an error to omit type
annotations for:
- methods
- instance variables
- class variables
- constants
It may seem counterintuitive that Sorbet does not require type annotations in
a file marked # typed: true
, but this is an intentional part of Sorbet's
implementation of gradual typing. In the # typed: true
strictness level, unannotated methods, instance variables, and constants are
assumed to be T.untyped
. This allows a programmer to write untyped or
partially-typed definitions while still benefiting from type checking when
static type information is present.