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?