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.