Skip to content

Latest commit

 

History

History
111 lines (92 loc) · 4.64 KB

untyped.md

File metadata and controls

111 lines (92 loc) · 4.64 KB
id title
untyped
T.untyped

The type T.untyped represents a type that Sorbet has no specific knowledge about. Instance variables and constants that have not been given a static type with T.let or another type assertion are assumed to have the type T.untyped, and methods that have not been given a signature with sig are assumed to take arguments of type T.untyped and return a value of type T.untyped.

Sorbet will not check any operations performed on values of type T.untyped: it will accept any method calls on them with any number of arguments of any types:

# typed: true
class A; end
A.new.foo   # Method foo does not exist on A
T.let(A.new, T.untyped).foo  # No errors

What is untyped?

In the absence of a sig, Sorbet will assume that the method takes values of T.untyped for all its parameters, and returns something of type T.untyped. Consequently, we first start using Sorbet with a codebase, it will be filled with T.untyped. When Sorbet examines a method without a sig, it can only deduce that the arguments to the method are of type T.untyped and that whatever is returned from the method is of type T.untyped.

It's also possible to use T.untyped as a type explicitly. For example, the following is a perfectly valid typed method definition:

extend T::Sig
# typed: true
sig { params(x: T.untyped, y: T.untyped).returns(T.untyped) }
def plus(x, y); x + y; end

In this case, the sig on plus is explicit version of the implicit signature that Sorbet would assume of plus in the absence of any other information. It would be easy for a programmer to deduce a reasonable static type for plus just by looking at the body of this simple method, but in a bigger codebase with more complicated methods, it can be useful to start with a type signature that includes parameters of type T.untyped and then replace those with more specific types as we go.

Converting from untyped

In general, there's nothing special that needs to be done to use an untyped value in a typed context. If a method accepts an Integer, then it will also accept a T.untyped value in the same place:

# typed: true
extend T::Sig
sig { params(x: Integer).returns(Integer) }
def incr(x); x + 1; end

n = T.let(5, T.untyped) # this value is untyped...
puts incr(n) # but Sorbet accepts it when passed to a
             # function that expects an Integer

In this case, it's obvious to a reader of the code that n has the expected type Integer at runtime, but what if it didn't? In that case, Sorbet will still accept the program during typechecking, but the program will produce an error at runtime due to Sorbet's runtime checks. Consequently, the method body of a method like incr can be guaranteed that it was passed parameters of the correct types, either because of compile-time validation or because we double-checked at runtime.

It's also possible to be more proactive about checking runtime types. This can be done with features like Ruby's is_a? method or case statement, which can be used to check whether something is an instance of a given class. Because Sorbet supports flow-sensitive typing, it can use these checks to refine type information in contexts where such tests have succeeded. In the example below, a starts out as T.untyped, but if a.is_a? Integer is true, then Sorbet can surmise that within the body of that conditional, a must be an Integer.

# typed: true
a = T.let(5, T.untyped)
T.reveal_type(a)  # a has type T.untyped

if a.is_a? Integer
  T.reveal_type(a)  # a has type Integer
end

Converting to untyped

There might be some situations in which we might want to "forget" static type information and treat a value as though it were untyped. The escape hatch Sorbet provides for this is called T.unsafe, which takes anything and returns that same thing but as T.untyped.

There's a reason this is named T.unsafe, and it is because converting things to T.untyped weakens static type guarantees, and should be done with caution. The value of a tool like Sorbet is that it can analyze code and produce guarantees about that code's behavior before that code ever runs, so weakening the static type information available to Sorbet will also weaken the guarantees that Sorbet provides. On the other hand, especially with codebases that have not been fully converted over to static types, it can be useful to have such an escape hatch. More specifics about how to use how to use T.unsafe and when it might be useful are explained in the troubleshooting section on T.unsafe.