Class Types
Note: Class types are used to describe values that are instances of a class—these are the most commonly used types. To instead learn about types for class objects themselves, see T.class_of.
Every Ruby class and module doubles as a type in Sorbet. Class types supersede
the notion some other languages have of “primitive” types. For example, "abc"
is an instance of the String
class, and so "abc"
has type String
. The same
goes for many other values in Ruby:
Type | Example value |
---|---|
String | "abc" |
Symbol | :abc |
Integer | 42 |
Float | 3.14 |
NilClass | nil |
To reiterate: a class type means "any value which is an instance of this class".
If x.is_a?(SomeClass)
would return true
when run, then x
has type
SomeClass
.
We can mention class types directly in a method signature:
sig {returns(Integer)}
def age
25
end
sig {params(x: Float).returns(String)}
def float_to_string(x)
x.to_s
end
Booleans
One gotcha is that false
is an instance of FalseClass
, and true
is an
instance of TrueClass
—there is no Boolean
class in Ruby. So to represent
the type of booleans in Sorbet, the sorbet-runtime
uses
type aliases and union types to define a
convenient name for "either true
or false
": T::Boolean
.
extend T::Sig
sig {params(new_value: T::Boolean).void}
def set_flag(new_value)
@flag = new_value
puts "Set value to #{new_value}"
end
set_flag(true)
set_flag(false)
nil
Note that the class (and type) of nil
is NilClass
.
There’s a lot to say about nil
, so it gets its own doc.
User-defined class types
Everything we’ve seen so far has used classes built into Ruby, but it works the exact same for any classes we define ourselves:
extend T::Sig
class MyClass; end
sig {returns(MyClass)}
def foo
MyClass.new
end
Inheritance
Ruby is object-oriented, so an instance of a child class is also an instance of the child class’s superclass. To make this more explicit, let’s look at an example:
extend T::Sig
# Set up an inheritance relationship between three classes
class GrandParentClass; end
class ParentClass < GrandParentClass; end
class ChildClass < ParentClass; end
# Takes ParentClass or lower, not GrandParentClass
sig {params(x: ParentClass).void}
def foo(x); end
foo(GrandParentClass.new) # error
foo(ParentClass.new) # ok
foo(ChildClass.new) # ok
Object
vs BasicObject
Another note about inheritance in Ruby concerns the distinction between Object
and BasicObject
. Object
is what classes subclass by default, unless they
explicitly subclass from BasicObject
. Object
subclasses from BasicObject
.
Again, let’s make this clear with an example:
# Some helper methods to play with
sig {params(x: Object).void}
def takes_object(x); end
sig {params(x: BasicObject).void}
def takes_basic_object(x); end
# The one error is because an instance of BasicObject is not an instance of Object
takes_object(Object.new) # ok
takes_object(BasicObject.new) # error
takes_basic_object(Object.new) # ok
takes_basic_object(BasicObject.new) # ok
# Some classes to play around with
class ObjectChild; end
class BasicObjectChild < BasicObject; end
# The class that explicitly subclasses `BasicObject`
# can't be given to `takes_object`.
takes_object(ObjectChild.new) # ok
takes_object(BasicObjectChild.new) # error
Modules
Modules can be used as “class types” in exactly the same way as classes can. For
a module, the meaning is not “an instance of this class” but "an instance of a
class which include
s this module". This is compatible with how
x.is_a?(SomeModule)
works in Ruby. Here’s an example:
extend T::Sig
module MyModule
def some_method
puts 'inside MyModule'
end
end
class MyClass
include MyModule
end
sig {params(x: MyModule).void}
def foo(x)
x.some_method
end
foo(MyClass.new) # ok; MyClass mixes in MyModule