You might not need switch in JavaScript
tl;dr: there is, but it's slower.
No switch, no party
Java folks are so fond of switch
, and so are JavaScript developers. Let's be honest, we developers are lazy, and for people like me who lacks creativity it's easy to stick with the status quo.
switch
is convenient: given an expression we can check if it matches with something else in a bunch of case
clauses. Consider this example:
const name = "Juliana";
switch (name) {
case "Juliana":
console.log("She's Juliana");
break;
case "Tom":
console.log("She's not Juliana");
break;
}
When the name is "Juliana" we print a message, and we immediately exit from the block with break
. When switch
is inside a function you may omit break
, as long as you return from each clause.
You can also have a default branch for when nothing matches:
const name = "Kris";
switch (name) {
case "Juliana":
console.log("She's Juliana");
break;
case "Tom":
console.log("She's not Juliana");
break;
default:
console.log("Sorry, no match");
}
switch
is also heavily used in Redux reducers (although Redux Toolkit simplified the boilerplate) to avoid a tonne of if
. Consider this example:
const LOGIN_SUCCESS = "LOGIN_SUCCESS";
const LOGIN_FAILED = "LOGIN_FAILED";
const authState = {
token: "",
error: "",
};
function authReducer(state = authState, action) {
switch (action.type) {
case LOGIN_SUCCESS:
return { ...state, token: action.payload };
case LOGIN_FAILED:
return { ...state, error: action.payload };
default:
return state;
}
}
What's wrong with it? Almost nothing. But is there a better alternative?
Learning from Python
This tweet from Telmo caught my eyes. He shows two styles for "switching", one of witch is so close to a similar pattern in Python.
Python has no switch, and it can teach us a better alternative to our beloved, cluttered switch. Let's first port our code from JavaScript to Python:
LOGIN_SUCCESS = "LOGIN_SUCCESS"
LOGIN_FAILED = "LOGIN_FAILED"
auth_state = {"token": "", "error": ""}
def auth_reducer(state=auth_state, action={}):
mapping = {
LOGIN_SUCCESS: {**state, "token": action["payload"]},
LOGIN_FAILED: {**state, "error": action["payload"]},
}
return mapping.get(action["type"], state)
In Python, we can simulate switch
with a dictionary. The default
case is basically replaced by the default for dict.get()
.
Python raises a KeyError
when you access an inexistent key:
>>> my_dict = {
"name": "John",
"city": "Rome",
"age": 44
}
>>> my_dict["not_here"]
# Output: KeyError: 'not_here'
The .get()
method is a safer alternative to direct key access, because it does not raise, and lets specify a default value for inexistent keys:
>>> my_dict = {
"name": "John",
"city": "Rome",
"age": 44
}
>>> my_dict.get("not_here", "not found")
# Output: 'not found'
So this line in Python:
# omit
return mapping.get(action["type"], state)
is equivalent in JavaScript to:
function authReducer(state = authState, action) {
// omit
default:
return state;
}
}
What's my point then? Let's apply the same Pythonic style to JavaScript.
An alternative to switch
Consider again the previous example:
const LOGIN_SUCCESS = "LOGIN_SUCCESS";
const LOGIN_FAILED = "LOGIN_FAILED";
const authState = {
token: "",
error: "",
};
function authReducer(state = authState, action) {
switch (action.type) {
case LOGIN_SUCCESS:
return { ...state, token: action.payload };
case LOGIN_FAILED:
return { ...state, error: action.payload };
default:
return state;
}
}
If we want to get rid of that switch
we can do something along these lines:
function authReducer(state = authState, action) {
const mapping = {
[LOGIN_SUCCESS]: { ...state, token: action.payload },
[LOGIN_FAILED]: { ...state, error: action.payload }
};
return mapping[action.type] || state;
}
Way much cleaner if you ask me. This nice refactoring is possible thanks to modern JavaScript, namely computed property names (ECMAScript 2015):
const mapping = {
[LOGIN_SUCCESS]: { ...state, token: action.payload },
[LOGIN_FAILED]: { ...state, error: action.payload }
};
Here properties for mapping
are computed on the fly from two constants: LOGIN_SUCCESS
and LOGIN_FAILED
. Then there is object spread (ECMAScript 2018):
const mapping = {
[LOGIN_SUCCESS]: { ...state, token: action.payload },
[LOGIN_FAILED]: { ...state, error: action.payload }
};
Here the values for this object are objects obtained from spreading the original state into a fresh object.
How do you like this approach? Of course, it has some limitations over a proper switch
, yet for a reducer could be a convenient technique.
But, how does this code performs?
How about performances?
Switch outperforms fancy syntax.
Performance wise switch
is unsurprisingly faster than the mapping version. You can do some sampling with the following snippet, just replace the version of authReducer
with the mapping version after testing switch:
console.time("sample");
for (let i = 0; i < 2000000; i++) {
const nextState = authReducer(authState, {
type: LOGIN_SUCCESS,
payload: "some_token"
});
}
console.timeEnd("sample");
Measure them ten times or so with:
for t in {1..10}; do node switch.js >> switch.txt;done
for t in {1..10}; do node map.js >> map.txt;done
Here's my results for ten consecutive samples (milliseconds):
Switch | Mapping |
---|---|
43.033 | 71.609 |
40.247 | 64.800 |
37.814 | 66.123 |
37.967 | 63.871 |
37.616 | 68.133 |
38.534 | 69.483 |
37.261 | 67.353 |
41.662 | 66.113 |
38.867 | 65.872 |
37.602 | 66.873 |
Feel free to draw your own conclusions. Thanks for reading!