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 super type 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.