Skip to content

Latest commit

 

History

History
199 lines (149 loc) · 5.53 KB

type-assertions.md

File metadata and controls

199 lines (149 loc) · 5.53 KB
id title
type-assertions
Type Assertions

There are four ways to assert the types of expressions in Sorbet:

  • T.let(expr, Type)
  • T.cast(expr, Type)
  • T.must(expr)
  • T.assert_type!(expr, Type)

There is also T.unsafe which is not a "type assertion" so much as an Escape Hatch.

T.let

A T.let assertion is checked statically and at runtime. In the following example, the definition of y will raise an error when Sorbet is run, and also when the program is run.

x = T.let(10, Integer)
T.reveal_type(x) # Revealed type: Integer

y = T.let(10, String) # error: Argument does not have asserted type String
→ View on sorbet.run

At runtime, a TypeError will be raised when the assignment to y is evaluated:

$ ruby test.rb
<...>/lib/types/private/casts.rb:15:in `cast': T.let: Expected type String, got type Integer with value 10 (TypeError)
Caller: test.rb:8
	from <...>/lib/types/_types.rb:138:in `let'
	from test.rb:8:in `<main>'

T.cast

Sometimes we the programmer are aware of an invariant in the code that isn't currently expressible in the Sorbet type system:

extend T::Sig

class A; def foo; end; end
class B; def bar; end; end

sig {params(label: String, a_or_b: T.any(A, B)).void}
def foo(label, a_or_b)
  case label
  when 'a'
    a_or_b.foo
  when 'b'
    a_or_b.bar
  end
end

In this case, we know (through careful test cases / confidence in our production monitoring) that every time this method is called with label = 'a', a_or_b is an instance of A, and same for 'b' / B.

Ideally we'd refactor the code to express this invariant in the types. To reiterate: the preferred solution is to refactor this code. The time spent adjusting this code now will make it easier and safer to refactor the code in the future. Even still, we don't always have the time right now, so let's see how we can work around the issue.

We can use T.cast to explicitly tell our invariant to Sorbet:

  case label
  when 'a'
    T.cast(a_or_b, A).foo
  when 'b'
    T.cast(a_or_b, B).bar
  end

Sorbet cannot statically guarantee that a T.cast-enforced invariant will succeed in every case, but it will check the invariant dynamically on every invocation.

T.cast is better than T.unsafe, because it means that something like

    T.cast(a_or_b, A).bad_method

will still be caught as a missing method statically.

T.must

T.must is for asserting that a value of a nilable type is not nil. T.must is similar to T.cast in that it will not necessarily trigger an error when srb tc is run, but can trigger an error during runtime. The following example illustrates two cases:

  1. a use of T.must with a value that Sorbet is able to determine statically is nil, that raises an error indicating that the subsequent statements are unreachable;
  2. a use of T.must with a computed nil value that Sorbet is not able to detect statically, which raises an error at runtime.
class A
  extend T::Sig

  sig {void}
  def foo
    x = T.let(nil, T.nilable(String))
    y = T.must(nil)
    puts y # error: This code is unreachable
  end

  sig {void}
  def bar
    vals = T.let([], T::Array[Integer])
    x = vals.find {|a| a > 0}
    T.reveal_type(x) # Revealed type: T.nilable(Integer)
    y = T.must(x)
    puts y # no static error
  end

end
→ View on sorbet.run

T.assert_type!

T.assert_type! is similar to T.let: it is checked statically and at runtime. It has the additional restriction that it will always fail statically if given something that's T.untyped. For example:

class A
  extend T::Sig

  sig {params(x: T.untyped).void}
  def foo(x)
    T.assert_type!(x, String) # error here
  end
end
→ View on sorbet.run

Comparison of type assertions

Here are some other ways to think of the behavior of the individual type assertions:

  • T.let vs T.cast

    T.cast(expr, Type)

    is the same as

    T.let(T.unsafe(expr), Type)
  • T.unsafe in terms of T.let

    T.unsafe(expr)

    is the same as

    T.let(expr, T.untyped)
  • T.must is like T.cast, but without having to know the result type:

    T.cast(nil_or_string, String)

    is the same as

    T.must(nil_or_string)