The Problem
Switches are ugly. They are bug prone. The default fallthrough behavior is begging for errors. I think Swift did the right thing and made fallthrough
a keyword, rather than a default, but even then, I'd rather avoid them if I can. They just don't fit in with the rest of my code. The indentation is awkward and no one can seem to decide if the case statements are indented or not.
Python didn't even bother to implement them in the language.
I primarily work in JavaScript, so I'll be focusing on that language. However, any language with first class functions and some sort of key/value pair structure can avoid switches. Java, for example, can use maps and lambdas. But I'll be sticking with JavaScript.
The JavaScript Object
How do we avoid switches? Well, each case in a switch is essentially a key-value pair. You're matching a single key, the case, with a single value, an expression to be evaluated, or a set of instructions. Sound familiar? Boil it down to two key works, key and value and you have a basic JavaScript object. How do we use a JavaScript object in place of a switch?
Well let's start with an example. Let's pretend we have some code that displays an error message when a log in fails.
errorMessage(error) {
const status = {
'401': 'Please check your username and password',
'404': 'Account not found, have you registered?',
'500': 'Something went wrong with the server, please try again later',
'Failed to fetch': 'Servers are currently down, please try again later'
}
return status[error.status];
}
Here we have code that behaves like a switch. We have 4 different error messages that can get thrown, a 401 if verification fails, a 404 if the user isn't found, a 500 if something broke, or a Failed to fetch
if the server is down. All we have to do is a very basic look up on an object and that's it. No fall through, no jarring switch
structure. Just a basic JavaScript object.
But what if I wanted a default case? Well that's simple too, we just need to check if the value is in the object itself. There are a number of ways to do this, but I'll just check if the property exists by checking for undefined
:
errorMessage(error) {
const status = {
'401': 'Please check your username and password',
'404': 'Account not found, have you registered?',
'500': 'Something went wrong with the server, please try again later',
'Failed to fetch': 'Servers are currently down, please try again later',
default: 'Something borked, sorry!'
}
if(!status[error.status]) {
return status['default'];
}
return status[error.status];
}
Current JavaScript is also fairly flexible. If I wanted to use numbers rather than strings for object keys, I can do that. JavaScript will, under the hood, turn them into strings. Therefore the following is also valid JavaScript:
const object = {
1: 'one',
2: 'two',
3: 'three'
}
object[1]; // 'one'
Of course, you can't use dot notation on this object, object.1
is invalid, but if we're simply using this object as a switch, then it doesn't matter. With bracket notation, dot notation isn't mandatory anyways. But what's important here is that we can recreate switch behavior with both strings and numbers. Now you could use true
and false
as strings for keys if you wanted to make a boolean, but I argue a switch is overkill for a boolean anyways.
Functions?
However, when we're using switch
, we're often doing more than grabbing strings and numbers, we might also be holding functions. Thankfully, JavaScript is a language that treats functions as first class citizens. Functions can be passed around like any other object, and of course, can be the values of properties in our objects.
Here arrow functions truly shine, though if you need to preserve this
, you'll have to reach for Function.prototype.bind()
, or use the old school syntax for JavaScript anonymous functions,function () { ...
. Function shorthand in JavaScript objects also preserve the context of this
and in that case, the name of the function, becomes the key, and the instruction block becomes its value.
const greet = {
sayHello1: function() { return 'Hello' },
sayHello2() { return 'Hello' },
sayHello3: ()=> { 'Hello' }
}
In this example, greet.sayHello1()
and greet.sayHello2()
do exactly the same thing. greet.sayHello3()
is slightly different because it is an arrow function and therefore the this
keyword is lost. However since the function isn't using this
, all three are exactly the same in this particular scenario. If you needed this
for an arrow function, you can do greet.sayHello3.bind(greet)
.
Imagine we have a text based RPG. You play a wizard who has a number of spells he can cast. The user types in the spell he wants and the wizard casts it. You could use a switch to determine which spell to cast, or use an object:
function castSpell(spellname) {
const spellbook = {
fireball: ()=> 'Wizard casts Fireball!',
iceshard: ()=> 'Wizard casts Ice Shard!',
arcanemissiles: ()=> 'Wizard casts Arcane Missiles',
polymorph: ()=> 'Wizard casts Polymorph!',
default: ()=> 'Wizard doesn\'t know that spell.'
}
if(!spellbook[spellname]) {
return spellbook['default']();
}
return spellbook[spellname]();
}
So what the function does is, you pass in a spell name, and it uses the spellname to match a value in the spellbook. That value is a function, so by using ()
after grabbing the value will call that function.
I'm using bracket notation for the default method here for consistency.
spellbook.default()
would also be valid.
Here we can call functions the same way we would in a switch. You can abstract away all the code that would be your case statements and shove them in an object methods and simply call them via bracket notation.
This does have some trade offs, as it's harder to tell what spellbook[spellname]()
is doing than case 'fireball': return fireball();
but the code is more elegant, there are fewer levels of indentation, and no threat of fallthrough.
But I Want Fallthrough!
Oh. Well then. Obtaining fallthrough behavior in an object is more difficult and there's no one way to do it. There could be an argument here where switch
might actually be a better construct to use. And if so, then use switch
. But understanding that objects have a number methods on them, there are other solutions as well. With Object.values()
, Object.keys()
and Object.entries()
, you can get all of your key/value pairs into arrays and then run them through any number of array functions. This can be done to achieve fallthrough.
Imagine we have an object with a bunch of functions and, given a number, we need to call all the functions upto, and not including that number. This is one case where a switch fallback is useful, but this is also easily done with an object. Here's an example:
function callFns(number) {
const object = {
1: ()=> console.log('one'),
2: ()=> console.log('two'),
3: ()=> console.log('three'),
4: ()=> console.log('four'),
5: ()=> console.log('five')
}
Object.keys(object).forEach(key => {
if(key >= number) {
object[key]();
}
});
}
Call callFns(3)
and it will log 'three'
, 'four'
, and 'five'
to the console. This would simulate using switch(3) {
with no break
or return
in any cases. By combining Object and Array methods, we can simulate a fallthrough for our situation. But again, this may be a case where a switch
might be the better construct. After all, the primary cause of bugs in a switch is the fallthrough feature. However, by using an Object, you get access to a number of methods that can make an object more flexible than a switch statement. By getting an array of your object's entries, you get access to filter
, reduce
, some
, every
, as well as iteration methods like map
and forEach
, and structures like for of
.
In Summary
Objects in JavaScript give you a simple alternative to the switch
statement. Objects are flexible and less bug prone than switch
statements and they aren't as jarring in your code as switch statements. If you don't want fallthrough, using an object in place of a switch is a better option. If you do want fallthrough, it can be achieved through Object and Array methods, but a regular switch
might be a better option.
All in all, your style of code is up to you, but I suggest being like Python and throwing the switch out entirely.
Happy Coding.
Top comments (16)
I can understand this from a JavaScript perspective, but this technique is very language specific. One of the HUGE reasons why switch statements exist in the first place is due to the way C/C++ compilers can optimize them behind the scenes. They're significantly faster to process if done correctly, because the compiler will build a jump table rather than a series of comparisons. In modern programming where we're just basically doing glue layers and performance isn't as critical, it doesn't matter much, but when you're working with microcontrollers or doing hardware drivers, it makes a world of difference.
Another advantage of switch statements is that some languages will allow for backwards comparisons. You can do something like "switch (true)" and then run a ton of cases to see which one is true first, and then process accordingly.
Sure, if the language you're working in has a lot of optimization for switches, then go with what works best for the language. My post was indeed mostly centered around JS. I know little about C++, so I won't presume to speak on it. If switches are better in C/C++, stick with them.
And even in JavaScript, if the situation is such that a switch is the simplest and most efficient way to achieve what you're looking for, then I'd remain with the structure.
With your example it works because values are non empty strings.
But since you are explaining a pattern here, it must be said that the check for default value is actually buggy.
Any falsy value will lead to the default "branch" because using "if" like this will check the value at key "x" and not if the key exists, unlike switch that actually matches against case.
You should be checking if the key is defined instead.
An example could be:
That's a good point.
Maybe you can do something like this:
This would not work. I have explained why above.
Switches, as you said, are an ugly and poorly implemented concept. This
object
toswitch
trick is fairly useful in cases you need lots of cases to check for.Call me crazy but, most of the time, I simply use a long
else if
chain when needing aswitch
. It feels natural, it's much more readable than theobject
one and it works in any language.All in all, very useful post!
Some language implementations have a limit to the number of "else if" statements you're allowed to use though.
Interesting. I never knew that. What languages have this limitation?
It isn't language specific, it is implemention specific. I know some C/C++ compilers have this issue, but not all.
For embedded I can see that limitation and that switch could look much more readable since you're mostly working with numbers (integers, addresses etc.) instead of more complex structures (objects, strings etc.)
If you want something like that, it really seems much less "magical" to simply use a class + methods.
Even in old JS this seems clearer:
I totally agree. Another cool option is to use Ramda's
cond
.You're barking up the wrong tree here. In day-to-day JavaScript practice (at least in my experience) there is very little actual object orientation involved. The frameworks do the OO stuff, "client" code is mostly object literals and JSON. Hardly anything has a prototype other than "object". And switch statements are all over the place. I don't agree with this style of programming, I'm with you on replacing switches with polymorphism. But I'm afraid the JS world won't listen.
An other point in favor of objects instead of switches, is that they are composables: we can spread the declaration of a big object through multiple files for a better code organisation.
It's a great pattern for example to keep Redux reducers gathered by domain / feature
Good idea, I love purity, but when looking for a bug with a debugger it doesnt stop on the code and does not make the error very clear. So I keep cases in and put demand for
break
in the linter.