T.anything
The type T.anything
is a type that is a supertype of all other types in Sorbet. In this sense, it is the “top” type of Sorbet’s type system.
sig { params(x: T.anything).returns(T.anything) }
def example(x)
x.nil? # error: `nil?` doesn't exist on `T.anything`
x
end
example(0) # ok
example('') # ok
In this example
method the parameter x
has type T.anything
, Sorbet lets it be called with anything. However, since Sorbet knows nothing about what methods exist on T.anything
, it rejects all methods calls on it (including .nil?
as a seen here).
T.anything
Doing something with T.anything
requires being explicitly downcast before it’s possible to do anything meaningful with a value of such a type:
sig { params(x: T.anything).void }
def print_if_even(x)
x.even? # error: Don't know whether `x` is an `Integer`
# option 1: safe downcast
case x
when Integer
if x.even?
puts("it's even")
else
puts("it's not even")
end
else
# ... handle case when not an `Integer` ...
end
# option 2: unchecked downcast
y = T.cast(x, Integer) # will raise at runtime if not an `Integer`
if y.even?
puts("it's even")
else
puts("it's not even")
end
end
In option 1, we use case
to check whether x
is an Integer
, which makes it easy to handle the case when x
is not an Integer
, too. Note that we have to use case
and not is_a?
, because is_a?
is a method, and T.anything
does not respond to any methods.
In option 2, we use T.cast
to do a runtime-only cast to raise an exception if x
is not an Integer
at runtime.
Viewed like this, T.anything
is a kind of forcing mechanism to require that consumers of some otherwise “untyped” interface do runtime type checks to verify that the type is what they expect.
T.anything
vs T.untyped
T.anything
is not the same as T.untyped
:
T.anything
is a supertype of all other types, but is not a subtype of any other type (except itself).T.untyped
is a supertype of all other types, and is a subtype of all other types (which is a contradiction that lies at the core of a gradual type system).
In simpler terms, Sorbet essentially assumes that a T.untyped
value is being used correctly. But for T.anything
, Sorbet does not allow treating it as if it were a specific type without some sort of runtime type check or cast.
To drive the difference home:
sig { params(x: Integer).void }
def takes_integer(x); end
sig do
params(
something_untyped: T.untyped,
could_be_anything: T.anything,
)
.void
end
def example(could_be_anything, something_untyped)
# OK, because `T.untyped` is a subtype of everything
takes_integer(something_untyped)
# NOT OK, because `T.anything` is only a subtype of `T.anything`,
# not `Integer` nor anything else
takes_integer(could_be_anything)
end
T.anything
vs BasicObject
BasicObject
is somewhat similar to T.anything
. In Ruby, BasicObject
is the parent class of all classes. But T.anything
is an even wider type than BasicObject
.
The distinction is subtle but important. For example, maybe we want to build an interface that only exposes a single method:
module IFoo
extend T::Helpers
abstract!
sig { abstract.void }
def foo; end
end
sig { params(x: IFoo, y: IFoo).void }
def example(x, y)
x.foo # obviously okay
x == y # should this be okay? (spoiler: it's not)
end
In this example: it’s totally fine to call x.foo
, because our IFoo
interface exposes a foo
method. But should the call to x == y
be allowed?
The ==
method isn’t in our interface. Technically speaking, BasicObject
defines ==
for all objects, but it’s not necessarily the case that it makes sense to compare all things that implement the IFoo
interface. As the author of this interface, we might actually want to have Sorbet tell us when we’re calling a method that’s not in the interface.
For this reason, Sorbet does not treat IFoo
as a subtype of BasicObject
. Programmers are free to build precisely the interface they’d like to expose, and never have to worry about “hiding” the methods from BasicObject
that they don’t want to expose.
This is why T.anything
is useful: there is still some type to write down in cases where truly passing in anything or returning anything is fine.
T.anything
vs T.type_parameter(:U)
Another common way to declare that a method “accepts anything” is to use a generic method:
sig { params(x: T.anything).void }
def takes_anything(x)
takes_anything_generic(x) # okay
end
sig do
type_parameters(:U)
.params(x: T.type_parameter(:U))
.void
end
def takes_anything_generic(x)
takes_anything(x) # okay
end
There are some subtle differences between these approaches, but overall they’re quite similar. In fact, the body of takes_anything_generic
is allowed to pass x
, which has type T.type_parameter(:U)
to takes_anything
. The opposite calling direction works as well.
So how are they different? Whenever using T.type_parameter(:U)
in a method signature, all occurrences of the type have to agree. In a method like the identify function, that means that the output has to be verbatim something that was provided as input:
sig { params(x: T.anything).returns(T.anything) }
def f(x)
# ...
end
sig do
type_parameters(:U)
.params(x: T.type_parameter(:U))
.returns(T.type_parameter(:U))
end
def identity(x)
res = f(x)
return res # error!
end
The signature for f
says that it takes T.anything
and returns T.anything
, but it is not required that the thing it returns is at all related to the thing it took as input.
Meanwhile, the signature for identity
says that it returns exactly what it was given as input. As such, it’s an error in the snippet above to have identity
return f(x)
instead of simply x
.
But for methods that only mention a given generic type parameter once (like our takes_anything
and takes_anything_generic
methods above), T.anything
and T.type_parameter(:U)
are nearly indistinguishable.
T.anything
and RBIs
Historically, Sorbet has favored being easy to adopt over avoiding T.untyped
. This means that certain methods, for example JSON.parse
, have been declared to return T.untyped
instead of T.anything
(or something more specific).
While we’re not opposed to using T.anything
in more places in RBI files, in each case we will judge it’s value against how costly it would be to adopt the RBI change. See the FAQ for more information about contributing RBI improvements.