| id | attr_reader |
|---|---|
| title | attr_reader, attr_writer, and attr_accessor |
| sidebar_label | attr_reader |
Sorbet has special support to allow defining signatures for Ruby's attr_reader, attr_writer, and attr_accessor methods.
sig { returns(Integer) }
attr_reader :foo
sig { params(bar: Integer).returns(Integer) }
attr_writer :bar
sig { returns(Integer) }
attr_accessor :quxFor attr_accessor, simply specify the return type. Sorbet will implicitly use the return type as the parameter type for the setter method defined by attr_accessor.
For attr_writer, the parameter name in the signature must be the same as the name of the attribute.
sig { returns(Integer) }
attr_accessor :x, :yRuby allows the attr_* methods to accept multiple symbols, defining multiple attribute methods.
Sorbet only allows using this form when all the attributes have the same type. In this form, one declared signature will be implicitly reused for all listed attribute names.
Otherwise, Sorbet requires that the attribute list is broken up to give unique types to each attribute:
sig { returns(Integer) }
attr_accessor :x
sig { returns(String) }
attr_accessor :yattr_*-defined methods are backed by instance variables. Sorbet requires types for instance variables in # typed: strict files.
To define types for these instance variables, either:
-
Ensure that the type for the attribute is T.nilable(...) and that there is a writer defined (via
attr_writerorattr_accessor), orsig { returns(T.nilable(Integer)) } attr_accessor :foo
-
Manually define the type for the instance variable, like any other instance variable.
sig { returns(Integer) } attr_accessor :foo sig { void } def initialize @foo = T.let(0, Integer) end
Note that due to limitations in Sorbet, the T.nilable must be visible syntactically. Hiding the T.nilable in a type alias or some other complicated type will not work:
NilableInteger = T.type_alias { T.nilable(Integer) }
sig { returns(NilableInteger) }
attr_accessor :foo
sig { void }
def example
p(@foo) # 💥 error: Use of undeclared variable `@foo`
endIt's unlikely that this restriction will be lifted in the future.
Adding a signature to an attribute only declares a runtime signature for the first method defined from that call to the attr_* method. Any method defined after will not have a runtime signature. For example:
# only defines a runtime signature for the getter method `foo()`
# DOES NOT define a runtime signature for the setter method `foo=(foo)`
sig { returns(Integer) }
attr_accessor :foo
# only defines a runtime signature for the first getter method `foo()`
# DOES NOT define a runtime signature for second/third getter methods (`bar()`, `qux()`)
sig { returns(Integer) }
attr_reader :foo, :bar, :quxThis is due to a technical limitation in sorbet-runtime, but which can hopefully be lifted in the future. See this issue for prior art.
Methods defined with attr_reader, attr_writer, or attr_accessor are shown specially in IDEs, like the document outline and the completion item list. They're reported as the property symbol kind, which in VS Code is shown with a 🔧 icon.
Sorbet does not track flow sensitivity for attribute-defined methods.
In Ruby, things defined by attr_reader, attr_writer, and attr_accessor are not "special": these DSLs just define normal methods. It just so happens that Ruby allows calling methods without trailing parentheses (e.g. foo()), so it sometimes looks like attribute-defined methods are special.
Sorbet can only track control flow-sensitive types on variables, not method calls. This is the exact same limitation that other popular gradual type checkers except for one difference: both JavaScript and Python make a syntactic distinction between method calls (which have parentheses) versus property/attribute access (which don't).
For more on how this affects Ruby, see this post: