Both ifs and switches are about picking one among few possible pieces of code.

In the case of an expression, the picked piece of code is evaluated and becomes the result of the expression. When it’s a statement, it’s expected to have some side effects, as the code is simply run.

Some languages, like Elm, are very strict. When there’s an if, there must be an else. When there’s a case (the Elm’s name for a switch expression), it must list all of the possible values. Other languages, like JavaScript, don’t require that.

There are also times when switch is faster than if.

I must admit that I don’t even remember when did I write my first ifs and switches. So it’s even more embarrassing to say that I discovered the most important difference between these two not so long ago. And it’s none of the things I mentioned above.

If picks one piece of code when a value is true (or truthy) and another when it’s false (or falsey). There are always only two inspected values: true and false.

Switch associates different pieces of code with different values. There may be many values and they are user defined.

Let’s take a look at a very simple example. It’s about loading something over the network. This is the code ifs encourage me to write:

if (state.isLoaded) {
  // ...
} else {
  // ...
  if (state.errorOccurred) {
    // ...
  } else {
    // ...
  }
  // ...
}

If I were writing this in TypeScript, it would be very tempting to define the state’s type like this:

let state: {isLoaded: boolean, errorOccurred: boolean};

Even though it may look legit at the first glance, it’s not.

The compiler will never notice that the following state is incorrect:

state = {isLoaded: true, errorOccurred: true};

The correct type looks more like this:

let state:
    {isLoaded: true, errorOccurred: false}
    | {isLoaded: false, errorOccurred: true}
    | {isLoaded: false, errorOccurred: false}
;

As we can see, what our application logic really cares about are not two boolean values, but three possible states.

let state: "loading" | "loaded" | "error";

As soon as we discover all the possible states, we can assign them different behaviors using a switch. There’s no need to inspect boolean values anymore.

switch (state) {
  case "loading":
    // ...
    break;
  case "error":
    // ...
    break;
  case "loaded":
    // ...
    break;
}

As little sense as it makes, we can bend an if to look like a switch, and vice versa:

if (state === "loading") {
  // ...
} else if (state === "error") {
  // ...
} else if (state === "loaded") {
  // ...
}
switch (state.isLoaded) {
  case true:
    // ...
    break;
  case false:
    // ...
    switch (state.errorOccurred) {
      case true:
        // ...
        break;
      case false:
        // ...
        break;
    }
    // ...
    break;
}

At this point it becomes clear that the difference between if and switch isn’t about the syntax. It’s about the mental model. Do I want to make decisions based on whether some expression evaluates to true, or do I want to discover all the possible states and assign them different behaviors?


What’s really fascinating is that the state discovery process is just the first step. Even though switches clearly present all the states, they hide state changes.

If we add the list of possible events which cause state transitions to the list of possible states, we get a state machine. And if we base our programming on a state machine, we are automata-based programmers!