Sorbet

Sorbet

  • Get started
  • Docs
  • Try
  • Community
  • GitHub
  • Blog

›Type System

Getting Started

  • Overview
  • Adopting Sorbet
  • Tracking Adoption
  • Quick Reference
  • Visual Studio Code
  • TypeScript ↔ Sorbet

Static & Runtime

  • Gradual Type Checking
  • Enabling Static Checks
  • Enabling Runtime Checks
  • RBI Files
  • Command Line Reference
  • Runtime Configuration

Troubleshooting

  • Troubleshooting
  • Why type annotations?
  • FAQ
  • Error Reference

Type System

  • sig
  • Type Annotations (non-sig)
  • T.let, T.cast, T.must, T.bind
  • Class Types (Integer, String)
  • Arrays & Hashes
  • Nilable Types (T.nilable)
  • Union Types (T.any)
  • Flow-Sensitivity (is_a?, nil?)
  • T.type_alias
  • Exhaustiveness (T.absurd)
  • T::Struct
  • T::Enum
  • T.untyped
  • Blocks, Procs, & Lambdas
  • Abstract Classes & Interfaces
  • Final Methods & Classes
  • Override Checking
  • Sealed Classes
  • T.class_of
  • T.self_type
  • T.noreturn
  • T.attached_class
  • Intersection Types (T.all)
  • Generics
  • T::NonForcingConstants

Experimental Features

  • Tuples
  • Shapes
  • Requiring Ancestors
Edit

Types for Class Objects via T.class_of

Classes are also values in Ruby. Sorbet uses T.class_of(...) to describe the types of those class objects.

T.class_of(Integer)

The difference between MyClass and T.class_of(MyClass) can be confusing. Here are some examples to make it less confusing:

These expressions……have these types
0, 1, 2 + 2Integer
IntegerT.class_of(Integer)
42.classT.class_of(Integer)

Here’s a playground link to confirm these types:

# typed: true
T.let(0, Integer)
T.let(1, Integer)
T.let(2 + 2, Integer)

T.let(Integer, T.class_of(Integer))
T.let(42.class, T.class_of(Integer))

→ View on sorbet.run

T.class_of and inheritance

As with Class Types, T.class_of types work with inheritance:

# typed: true
extend T::Sig

class Grandparent; end
class Parent < Grandparent; end
class Child < Parent; end

sig {params(x: T.class_of(Parent)).void}
def example(x); end

example(Grandparent)   # error
example(Parent)        # ok
example(Child)         # ok

→ View on sorbet.run

The most surprising feature of T.class_of comes from not understanding inheritance in Ruby, especially with include or extend plus modules.

See below for a common gotcha.

T.class_of and modules

TL;DR: T.class_of has some unintuitive behavior with modules (as opposed to classes). Consider either using an abstract class or using T.all(Class, MyInterface::ClassMethods) instead of T.class_of(MyInterface).

To showcase the problem and solutions, let’s walk through a running example. The full code for this example is available here:

→ View on sorbet.run

Suppose we have some code like this:

class MyClass
  def some_instance_method; end
  def self.some_class_method; end
end

sig {params(x: T.class_of(MyClass)).void}
def example1(x)
  x.new.some_instance_method  # ok
  x.some_class_method         # ok
end

example1(MyClass)             # ok

MyClass declares a class which has an instance method and a class method. The T.class_of(MyClass) annotation allows example1 to call both those methods. None of this is too surprising.

Now imagine that we have a lot of these classes and we want to factor out an interface. The straightforward way to do this uses mixes_in_class_methods, like this:

module MyInterface
  extend T::Helpers

  def some_instance_method; end

  module ClassMethods
    def some_class_method; end
  end
  mixes_in_class_methods(ClassMethods)
end

class MyClass
  include MyInterface
end

This will make some_instance_method and some_class_method available on MyClass, just like before. But if we try to replace T.class_of(MyClass) with T.class_of(MyInterface), it doesn’t work:

sig {params(x: T.class_of(MyInterface)).void}  # ← sig has changed
def example2(x)
  x.new.some_instance_method  # error: `new` does not exist
  x.some_class_method         # error: `some_class_method` does not exist
end

example2(MyClass)             # error: Expected `T.class_of(MyInterface)`
                              #        but found `T.class_of(MyClass)`

These errors are correct, and we can verify them in the Ruby REPL. First, let’s explain the error on the last line above:

❯ MyClass.singleton_class.ancestors
=> [#<Class:MyClass>, MyInterface::ClassMethods, #<Class:Object>, T::Private::Methods::MethodHooks, #<Class:BasicObject>, Class, Module, T::Sig, Object, Kernel, BasicObject]

The first two ancestors of the MyClass object are itself and MyInterface::ClassMethods. But notably, #<Class:MyInterface> does not appear in this list, so Sorbet is correct to say that MyClass does not have type T.class_of(MyInterface). This is because neither include nor extend in Ruby will cause #<Class:MyInterface> to appear in any ancestors list.

Next, let’s explain the other two errors:

❯ MyInterface.singleton_class.ancestors
=> [#<Class:MyInterface>, T::Private::MixesInClassMethods, T::Helpers, Module, T::Sig, Object, Kernel, BasicObject]

For the MyInterface class object, we see that its only ancestor is itself (ignoring common ancestors like Object). Notably, none of the classes in this list define either a method called new (because Class is not there) nor some_class_method (because MyInterface::ClassMethods is not there).

While these errors are technically correct, we want to be able to type this code. There are two options:

  1. Use an abstract class instead of an interface.

    Sometimes this is not possible, because the class in question already has a superclass that can’t be changed. However, if this option is available, it’s likely the most straightforward. If we change MyInterface to MyAbstractClass, all our problems vanish.

  2. Use T.all(Class, MyInterface::ClassMethods).

    For our example this is only a partial solution, but in many cases it is good enough.

Specifically, option (2) looks like this:

sig {params(x: T.all(Class, MyInterface::ClassMethods)).void}
def example3(x)
  x.new.some_instance_method  # error: `some_instance_method` does not exist
  x.some_class_method         # ok
end

example3(MyClass)             # ok

We’re down to only one error now. The error is still technically correct: since we’re using Class instead of T.class_of(...), Sorbet has no way to know what the instance type created by x.new will be (it could be anything), so it treats the type as Object, causing some_instance_method to not be found. However, both the top-level call site to example3 and the call to x.some_class_method now typecheck successfully. In cases where we don’t actually need to use instance methods from MyInterface, this may be an acceptable workaround.

A future feature of Sorbet might be able to improve this workaround. See https://github.com/sorbet/sorbet/issues/62.

← Sealed ClassesT.self_type →
  • T.class_of and inheritance
  • T.class_of and modules

Get started · Docs · Try · Community · Blog · Twitter