Sometimes you might want a Sorbet method signature like, “Returns
T.nilable
only if the input is
T.nilable
. But if the input is non-nil
, then
so is the output.”
With clever usage of Sorbet generics, this is possible!
sig do
type_parameters(:U)
.params(
amount: T.any(T.all(T.type_parameter(:U), NilClass), Amount)
)
.returns(T.any(T.all(T.type_parameter(:U), NilClass), String))
end
def get_currency(amount)
if amount.nil?
return amount
else
return amount.currency
end
end
And then calling this method looks like this:
sig do
params(
amount: Amount,
maybe_amount: T.nilable(Amount),
nil_class: NilClass
)
.void
end
def example(amount, maybe_amount, nil_class)
res = get_currency(amount)
T.reveal_type(res) # => String
res = get_currency(maybe_amount)
T.reveal_type(res) # => T.nilable(String)
res = get_currency(nil_class)
T.reveal_type(res) # => T.nilable(String)
end
Some things to note about how it works:
It’s using Sorbet’s support for generic methods.
In other languages, you might try to do something like this with overloaded signatures, but Sorbet doesn’t support overloads, except in limited circumstances.
It
uses T.all
(an intersection type)
to approximate bounded
method generics.
For the time being, Sorbet doesn’t have first class support for placing bounds on generic method type parameters, so we have to approximate them with intersection types.
The upper bound
is NilClass
, not T.nilable(Amount)
!
This is the main trick. This lets us either return the thing
we wanted (in our case, String
), or the input if
the input is nil
.
.all(T.type_parameter(:U), NilClass) T
Returning something with a generic type means returning the exact
input,This constraint about returning the input unchanged
is an example of parametricity in
action.
not merely “something with the same type as the
input.”
So instead of having return nil
on line 10, we have to
write return amount
.
When we call get_currency(amount)
, where
amount
is known to be non-nil
, Sorbet is smart
enough to know that get_currency
returns
String
!
What’s happening here is that Sorbet is inferring the
T.type_parameter(:U)
to T.noreturn
, which then
collapses into String
, because
T.any(T.noreturn, String)
is just String
.