A switch statement smells.
For several reasons, and you should never use it.
Let's make some arguments:
Let's get started with the main argument of this article:
It is verbose and WET
Look at the following example:
Example 1
const quantity = 1;
switch (quantity) {
case 0:
console.log('You have none. Sorry');
break;
case 1:
console.log('You have some. Keep going!');
break;
case 2:
console.log('Someone here is rich!');
break;
default:
console.log('No cases matched');
}
It shows a very common use of a switch. In this example the intent is to log different messages based on a quantity.
Repetitions are everywhere:
- Every value is prefixed with
case
; - Every log message is wrapped in a
console.log
call; - Every
case
block is ended with abreak
;
What happens in the example is basic mapping.
We should use a map to map things.
In JS an object is a map. A
Map
class is also available with different APIs.
Here is the revised code:
Revised 1
const map = {
0: 'You have none. Sorry',
1: 'You have some. Keep going!',
2: 'Someone is rich!',
};
console.log(map[quantity] ?? 'No cases matched');
Benefits
- The mapping overhead goes down to the bare minimum, the association case -> output is addressed by a simple colon. It can't get *DRY*er than that (We could eventually use tuples)
- The
log
is invoked only once with the proper map index as content - A nullish coalescing operator (
??
) or a logical OR (||
) takes care of the default value in the case of no matches - All the repetitions found in the original example are gone. After all, all we wanna do is log something
- No
break
ordefault
keywords are needed - the intent of mapping a quantity to a message is much more clear
- No 2-deep nesting needed
- 161 chars vs 319 (helps with readability)
- Last but not least while the invocation of the
console.log
is still a statement, its argument is now an expression. Which can be easily extracted and given a name. The declaration of the map is technically also a statement here, but it could easily be an expression if inlined or passed as an argument (see Chapter 3).
Counter-intuitive fall-trough behavior
A switch statement can do several different things based on how we use it (and this is not necessarily a good thing). It is broken by design and can be a nightmare to decode.
Let's have a look at an example that leverages the fall-trough directly from MDN doc page:
Example 2
const animal = 'Giraffe';
switch (animal) {
case 'Cow':
case 'Giraffe':
case 'Dog':
case 'Pig':
console.log('This animal is not extinct.');
break;
case 'Dinosaur':
default:
console.log('This animal is extinct.');
}
This is a common pattern to have several values triggering the same behaviour, but it works because of a secret rule that says: "if you leverage fall-trough you still need a break after doing something". Omitting the break
after the first console.log
would trigger the second too, despite the case not being 'Dinosaur'
or unmatched (default
).
Let's verify this behaviour with a distilled example:
Example 3
switch (1) {
case 1:
case 2:
console.log('either: 1, 2');
break;
case 3:
case 4:
console.log('either: 3, 4');
break;
default:
console.log('something else');
}
// logs "either: 1, 2"
The output is what we expect, thanks to the break after the first log. We are using the fall-trough just trough cases, not through their code blocks and this is fundamental. If we get rid of the break
s entirely once a condition is met, all possible actions for every subsequent case (even unmatched will fire).
Let's simplify again:
Example 4
switch (2) {
case 1:
console.log('1');
case 2:
console.log('2');
case 3:
console.log('3');
case 4:
console.log('3');
}
// outputs 2, 3, 4 because we are switching on 2, if we were switching on 1 it would log 1, 2, 3, 4
Things can get even absurder when we switch
on true
which is a common pattern to assess a bunch of conditions that need subsequent actions.
Another example from same MDN page:
Example 5
switch (true) {
case "fetch" in globalThis:
// Fetch a resource with fetch
break;
case "XMLHttpRequest" in globalThis:
// Fetch a resource with XMLHttpRequest
break;
default:
// Fetch a resource with some custom AJAX logic
break;
}
This works because fall-trough is prevented and because the two conditions for the two cases are mutually exclusive.
The mechanism of the switch works so that an expression (yes, not just values) is switched onto and every case also presents an expression (again, not just values). The two expressions are strictly compared (using triple equal sign) to check a match.
We could not have a fall-trough implementation when switching on true
:
Example 6
switch(true) {
case false:
console.log('Log false');
case 1:
console.log('Log 1');
case true:
console.log('Log true');
}
// logs: Log true (expected)
This code barely stands as the matching case is last...
But what happens if we move the last case in the first position is not so expected:
Example 7
switch(true) {
case true:
console.log('Log true');
case false:
console.log('Log false');
case 1:
console.log('Log 1');
}
// logs: Log true, Log false, Log 1
You can obviously deal with it if you know how it exactly works but this is just a bad API no excuse, and should be avoided.
Let's now rewrite the animals example using a map:
const mapAnimalsExtinct = {
Cow: false,
Giraffe: false,
Dog: false,
Pig: false,
Dinosaur: true,
};
console.log(
mapAnimalsExtinct[animal]
? 'This animal is indeed extinct'
: 'This guy is alive and kicking'
);
The values are now repeated but that's content and much more database-y, so to be expected. Repetition in data has nothing to do with repetition in code as it is declarative and declarative code depicts intents and content.
To minimize it further we could annotate only the extinct animals from the original complete list.
It is a statement, not an expression
- Statements do things, expressions are evaluated down to values.
- Statements can be placed only in the body of code block, expressions can be place anywhere (inlined anywhere, arguments default values, etc.).
- Expressions are referentially transparent and always correspond to their computed value.
- Many statements can be replaced by expressions.
The topic is complex and will deserve its own article.
Conclusion
Switch is archaic, verbose, repetitive and poorly designed.
Since most often it is used to map certain values to certain behaviors, we can use maps instead, having a leaner and immediately readable code.
Thanks.
I think the only use of a switch is when you use return. for example
No erroneous fall-through here.
I would not solve every switch statement with a map.
The animals example could also be rewritten as
This code is more future proof. In case of the map with the boolean, you have to rewrite everything if there is a request to add almost extinct animals.