Working with nilable types

Since Crystal supports union types, it’s possible to have methods whose returns are nilable.

Here’s a method with a return signature of (String | Nil) or in short String?.

def maybe_string : String?
  condition = true
  if condition
    "A string..."
  else
    nil
  end
end

And say we want to upcase the returned String:

val = maybe_string

p! val.upcase  # => Error: undefined method 'upcase' for Nil (compile-time type is (String | Nil))

The result would be an error because the compiler wouldn’t be able to guarantee that the return type is a String and not Nil. If the return type is in fact Nil, then it would not have an upcase method available.

To avoid this error, we can check for nil with the tools Crystal provides. Here’s at least three different ways we could do this:

def maybe_string : String?
  condition = true
  if condition
    "A string..."
  else
    nil
  end
end
val = maybe_string

# Method one - Check if `val` is truthy:
p! val.upcase if val

# Method two - Check if `val` is nil:
p! val.upcase unless val.nil?

# Method three - Check if `val` is nil by usinkg `try`:
p! val.try &.upcase
#OUTPUT:
val.upcase # => "A STRING..."
val.upcase # => "A STRING..."
val.try(&.upcase) # => "A STRING..."

We managed to call upcase in all three cases because maybe_string actually returned String and we satisfied the compiler by branching in a way that our code was called only when maybe_string returned a String.

  1. In the first case, we just checked if val was truthy, because it wouldn’t be truthy if val was Nil.
  2. In the second case we used a method provided by the compiler, .nil?1, this would return true if val was Nil.
  3. And finally on the third case, we’ve used another built-in method try2. try only executes the passed code block if val is not Nil.