Skip to content

if

An if evaluates the given branch if its condition is truthy. Otherwise, it evaluates the else branch if present.

a = 1
if a > 0
  a = 10
end
a # => 10

b = 1
if b > 2
  b = 10
else
  b = 20
end
b # => 20

elsif

To write a chain of if-else-if you use elsif:

if some_condition
  do_something
elsif some_other_condition
  do_something_else
else
  do_that
end

Variable types

After an if, a variable’s type depends on the type of the expressions used in both branches.

a = 1
if some_condition
  a = "hello"
else
  a = true
end
# a : String | Bool

b = 1
if some_condition
  b = "hello"
end
# b : Int32 | String

if some_condition
  c = 1
else
  c = "hello"
end
# c : Int32 | String

if some_condition
  d = 1
end
# d : Int32 | Nil

Note that if a variable is declared inside one of the branches but not in the other one, at the end of the if it will also contain the Nil type.

Inside an if's branch the type of a variable is the one it got assigned in that branch, or the one that it had before the branch if it was not reassigned:

a = 1
if some_condition
  a = "hello"
  # a : String
  a.size
end
# a : String | Int32

That is, a variable’s type is the type of the last expression(s) assigned to it.

If one of the branches never reaches past the end of an if, like in the case of a return, next, break or raise, that type is not considered at the end of the if:

if some_condition
  e = 1
else
  e = "hello"
  # e : String
  return
end
# e : Int32

As a suffix

An if can be written as an expression’s suffix:

a = 2 if some_condition

# The above is the same as:
if some_condition
  a = 2
end

This sometimes leads to code that is more natural to read.

As an expression

The value of an if is the value of the last expression found in each of its branches:

a = if 2 > 1
      3
    else
      4
    end
a # => 3

If an if branch is empty, or it’s missing, it’s considered as if it had nil in it:

if 1 > 2
  3
end

# The above is the same as:
if 1 > 2
  3
else
  nil
end

# Another example:
if 1 > 2
else
  3
end

# The above is the same as:
if 1 > 2
  nil
else
  3
end

Ternary if

The ternary if allows writing an if in a shorter way:

a = 1 > 2 ? 3 : 4

# The above is the same as:
a = if 1 > 2
      3
    else
      4
    end

if var

If a variable is the condition of an if, inside the then branch the variable will be considered as not having the Nil type:

a = some_condition ? nil : 3
# a is Int32 or Nil

if a
  # Since the only way to get here is if a is truthy,
  # a can't be nil. So here a is Int32.
  a.abs
end

This also applies when a variable is assigned in an if's condition:

if a = some_expression
  # here a is not nil
end

This logic also applies if there are ands (&&) in the condition:

if a && b
  # here both a and b are guaranteed not to be Nil
end

Here, the right-hand side of the && expression is also guaranteed to have a as not Nil.

Of course, reassigning a variable inside the then branch makes that variable have a new type based on the expression assigned.

Limitations

The above logic works only for local variables. It doesn’t work with instance variables, class variables, or variables bound in a closure. The value of these kinds of variables could potentially be affected by another fiber after the condition was checked, rendering it nil. It also does not work with constants.

if @a
  # here `@a` can be nil
end

if @@a
  # here `@@a` can be nil
end

a = nil
closure = ->{ a = "foo" }

if a
  # here `a` can be nil
end

This can be circumvented by assigning the value to a new local variable:

if a = @a
  # here `a` can't be nil
end

Another option is to use Object#try found in the standard library which only executes the block if the value is not nil:

@a.try do |a|
  # here `a` can't be nil
end

Method calls

That logic also doesn't work with proc and method calls, including getters and properties, because nilable (or, more generally, union-typed) procs and methods aren't guaranteed to return the same more-specific type on two successive calls.

if method # first call to a method that can return Int32 or Nil
  # here we know that the first call did not return Nil
  method # second call can still return Int32 or Nil
end

The techniques described above for instance variables will also work for proc and method calls.

if var.is_a?(...)

If an if's condition is an is_a? test, the type of a variable is guaranteed to be restricted by that type in the then branch.

if a.is_a?(String)
  # here a is a String
end

if b.is_a?(Number)
  # here b is a Number
end

Additionally, in the else branch the type of the variable is guaranteed to not be restricted by that type:

a = some_condition ? 1 : "hello"
# a : Int32 | String

if a.is_a?(Number)
  # a : Int32
else
  # a : String
end

Note that you can use any type as an is_a? test, like abstract classes and modules.

The above also works if there are ands (&&) in the condition:

if a.is_a?(String) && b.is_a?(Number)
  # here a is a String and b is a Number
end

The above doesn’t work with instance variables or class variables. To work with these, first assign them to a variable:

if @a.is_a?(String)
  # here @a is not guaranteed to be a String
end

a = @a
if a.is_a?(String)
  # here a is guaranteed to be a String
end

# A bit shorter:
if (a = @a).is_a?(String)
  # here a is guaranteed to be a String
end

if var.responds_to?(...)

If an if's condition is a responds_to? test, in the then branch the type of a variable is guaranteed to be restricted to the types that respond to that method:

if a.responds_to?(:abs)
  # here a's type will be reduced to those responding to the 'abs' method
end

Additionally, in the else branch the type of the variable is guaranteed to be restricted to the types that don’t respond to that method:

a = some_condition ? 1 : "hello"
# a : Int32 | String

if a.responds_to?(:abs)
  # here a will be Int32, since Int32#abs exists but String#abs doesn't
else
  # here a will be String
end

The above doesn’t work with instance variables or class variables. To work with these, first assign them to a variable:

if @a.responds_to?(:abs)
  # here @a is not guaranteed to respond to `abs`
end

a = @a
if a.responds_to?(:abs)
  # here a is guaranteed to respond to `abs`
end

# A bit shorter:
if (a = @a).responds_to?(:abs)
  # here a is guaranteed to respond to `abs`
end

if var.nil?

If an if's condition is var.nil? then the type of var in the then branch is known by the compiler to be Nil, and to be known as non-Nil in the else branch:

a = some_condition ? nil : 3
if a.nil?
  # here a is Nil
else
  # here a is Int32
end

if !

The ! operator returns a Bool that results from negating the truthiness of a value.

When used in an if in conjunction with a variable, is_a?, responds_to? or nil? the compiler will restrict the types accordingly:

a = some_condition ? nil : 3
if !a
  # here a is Nil because a is falsey in this branch
else
  # here a is Int32, because a is truthy in this branch
end
b = some_condition ? 1 : "x"
if !b.is_a?(Int32)
  # here b is String because it's not an Int32
end

unless

The unless clause works exactly like if but with negated condition. It matches if the condition is falsey. unless x is equivalent to if !x.

unless some_condition
  expression_when_falsey
else
  expression_when_truthy
end

# The above is the same as:
if !some_condition
  expression_when_falsey
else
  expression_when_truthy
end

unless does not have an equivalent to elsif, but otherwise supports the same features as if, including suffix notation:

close_door unless door_closed?