3

Based on: https://sorbet.org/docs/type-assertions

In my mind, T.cast seems like a more powerful version of T.let. T.cast(expr, type) forces Sorbet to accept expr as type, even if expr cannot be statically verified as type. T.let(expr, type) hints to Sorbet that expr has type, and Sorbet will be able to typecheck that hint is valid. Based on this understanding, I would assume that T.cast could always be used in place of T.let, but T.let is preferred because Sorbet is providing the most type safety. However, I recently found a case where that is not true (see below).

This example is based on: https://sorbet.org/docs/error-reference#7001

# typed: true

class Main
  extend T::Sig

  sig { params(elem: Integer).returns(T::Boolean) }
  def valid?(elem)
    elem == 0
  end

  sig { params(list: T::Array[Integer]).returns(T::Boolean) }
  def works(list)
    # Declare `all_valid` with type `T::Boolean`
    all_valid = T.let(false, T::Boolean)

    list.each do |elem|
      # Does not change the type of `all_valid`
      all_valid &&= valid?(elem) # ok
    end

    all_valid
  end

  sig { params(list: T::Array[Integer]).returns(T::Boolean) }
  def does_not_work(list)
    all_valid = T.cast(false, T::Boolean)

    list.each do |elem|
      # ERROR! Changing the type of a variable in a loop is not permitted
      all_valid &&= valid?(elem)
    end

    all_valid
  end
end

Why does this work with T.let but not with T.cast?

Billy the Kid
  • 271
  • 2
  • 12

1 Answers1

1

The T.let annotation explicitly widens the type from FalseClass to T:Boolean

T.cast on the other hand simply relaxes Sorbet’s static checks, it doesn’t silence type errors at runtime that occur from treating TrueClass or FalseClass as T::Boolean.

Fraser
  • 15,275
  • 8
  • 53
  • 104