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
  • CLI Quickstart
  • CLI Reference
  • Runtime Configuration

Troubleshooting

  • Troubleshooting
  • Why type annotations?
  • FAQ
  • Error Reference
  • Unsupported Ruby Features

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.anything
  • T.attached_class
  • Intersection Types (T.all)
  • Generics
  • T::NonForcingConstants
  • Banning untyped

Ruby & DSL Features

  • attr_reader
  • minitest

Editor Features

  • Language Server (LSP)
  • Server Status
  • LSP & Typed Level
  • Go to Definition
  • Hover
  • Autocompletion
  • Find All References
  • Code Actions
  • Outline & Document Symbols
  • Documentation Comments
  • Suggesting sigs
  • Highlighting untyped
  • sorbet: URIs

Experimental Features

  • Tuples
  • Shapes
  • Overloads
  • Requiring Ancestors
  • RBS Comments
Edit

T.self_type

Warning: This feature is experimental and has known limitations. It may not work as expected or change without notice. See related issues.

T.self_type

This type can be used in return types to indicate that calling this method on will return the same type as the type of the receiver (the receiver is the thing we call a method on i.e., x in x.foo). For instance, #dup returns T.self_type. No matter what class you call it on, you will get back the same type.

# typed: true

class Parent
  extend T::Sig

  sig { returns(T.self_type) }
  def foo
    self
  end
end

class Child < Parent; end

T.reveal_type(Parent.new.foo) # Revealed type: Parent
T.reveal_type(Child.new.foo) # Revealed type: Child

module Mixin
  extend T::Sig

  sig { returns(T.self_type) }
  def bar
    self
  end
end

class UsesMixin
  extend Mixin
end

T.reveal_type(UsesMixin.bar) # Revealed type: T.class_of(UsesMixin)

Note that T.self_type declares that the type should be exactly what the type of the receiver of the method is. It is not useful for declaring the type of factory methods. For these types of methods, use T.attached_class instead:

class Parent
  # sig { returns(T.self_type) } # WRONG
  sig { returns(T.attached_class) }
  def self.make
    new
  end
end

class Child < Parent; end

T.reveal_type(Parent.make) # => `Parent`
T.reveal_type(Child.make)  # => `Child`

If the above example was declared using T.self_type, Sorbet would expect to see the make method return a value of type T.class_of(Parent), but new returns a value of type Parent (an instance of the class, not the class object itself).

Do not use T.self_type for ==

It’s tempting to use T.self_type in the definition of an object’s == method, but this is incorrect.

sig { params(other: T.self_type).returns(T::Boolean) }
#            ^^^^^^^^^^^^^^^^^^ ⛔ Incorrect!
def ==(other)
end

Ruby equality methods must accept values of an arbitrary class, even if they will return false when self.class != other.class. As such, equality methods must accept something broad, like BasicObject or T.anything:

sig { params(other: T.anything).returns(T::Boolean) }
def ==(other)
end

For more, see this FAQ entry

T.self_type is not checked at runtime

T.self_type does not get runtime type checking like most other types. In this way, it behaves similarly to how generic types are runtime-erased and thus not runtime checked.

Limitations

Methods that return T.self_type are not checked precisely

Sorbet incorrectly typechecks methods declared to return T.self_type:

class Parent
  sig { returns(T.self_type) }
  def returns_same_type
    Parent.new # ‼️ incorrect!
  end
end

class Child < Parent; end

p = Parent.new.returns_same_type
T.reveal_type(p) # => `Parent`

c = Child.new.returns_same_type
T.reveal_type(c) # => `Child`

The meaning of T.self_type is “whatever the type is of the thing the method was called on,” or the method’s receiver. The method’s receiver varies. A method might be defined on Parent but called on Child, in which case T.self_type is equivalent to Child.

Sorbet models this relationship at call to methods that return T.self_type, but does not check this property inside methods bodies that return T.self_type. There is an uncaught bug in the snippet above: Sorbet allows returns_same_type to unconditionally return Parent.new, which means that calls on subclasses will have an inferred type that does not match what the real class is at runtime.

Methods that return T.self_type should use self to compute the value that is eventually returned. For example, all of these are valid ways to use self to return something of type T.self_type:

  self # ✅
  self.class.new # ✅
  self.clone # ✅
  # ...

Sorbet does not yet check this property but will one day. Please follow or upvote this issue.

Only top-level

Sorbet only supports T.self_type at the top-level of a type, e.g. not nested inside a generic class type:

class Generic < Parent
  extend T::Generic
  TM = type_member

  sig { returns(Generic[T.self_type]) } # error: Only top-level `T.self_type` is supported
  def bad
    Generic[T.untyped].new
  end
end

Since T.proc types are basically generic class types in disguise, T.self_type cannot be used inside T.proc parameter or return types (though using T.self_type in T.proc.bind is allowed). This means that methods like yield_self and tap cannot be precisely typed yet.

Note that “top-level” in this context only applies to inside the type arguments applied to generic class types—T.self_type can already be nested inside types like T.any and T.all.

This limitation is likely to be lifted eventually, so please let us know whether that’s important.

No uses in parameters’ types

Using T.self_type in a method’s parameter is rejected:

class A
  sig { params(other: T.self_type).void }
  def foo(other)
    other
  end
end

One exception is if the method is private:

class Parent
  sig { params(other: T.self_type).void }
  private_class_method def inherited(other)
    other
  end
end

If it helps, think of T.self_type as a type_member that is covariant and upper bounded by itself (i.e., a recursively-defined type). Just as covariant type members are not allowed in input positions, neither is T.self_type, unless the method is private.

← T.class_ofT.noreturn →
  • Methods that return T.self_type are not checked precisely
  • Only top-level
  • No uses in parameters' types

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