Only return nil if given nil

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
      T.any(T.all(T.type_parameter(), NilClass), Amount)
    .returns(T.any(T.all(T.type_parameter(), NilClass), String))
def get_currency(amount)
  if amount.nil?
    return amount
    return amount.currency
Full example on →

And then calling this method looks like this:

sig do
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)

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.

T.all(T.type_parameter(), NilClass)

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.