If you're a software developer, chances are you'll use the switch
stament at some point. This operator allows you to evaluate the expression and execute different blocks of code depending on the value of said expression. Although switch is a useful tool, overusing it can lead to code that is difficult to maintain and modify. In this article we explain why you should consider moving away from switch and some alternatives you can use to improve the readability and maintainability of your code.
Problem
In this example, the notifier function takes a notification argument of type Notification
, which is an enum representing different notifications. The function uses a switch statement to determine which notification to send based on the value of the Notification
argument. We see an example:
You can take a look at why it is not recommended to use Enums here
const enum Notification {
verifySignUp,
resendVerifySignup,
emailConfirmed
}
export function notifier (notification: Notification) {
switch (notification) {
case Notification.verifySignUp:
// Execute something...
return;
case Notification.resendVerifySignup:
// Execute something...
return;
case Notification.emailConfirmed:
// Execute something...
return
}
}
As the number of cases within the switch increases, the readability of the code suffers. Also, if a new fruit is added to the enum, the function must be updated to handle the new case, which can lead to errors if you forget to update it.
There are more scalable and maintainable alternatives to switch in this case, such as using an object that contains the colors of the fruits. This also makes the code more readable and scalable as more fruits are added.
Another possible alternative that I have found in other projects is the use of if.
export function notifier(notification: Notification) {
if (notification === Notification.verifySignUp) {
// Execute something...
return;
}
if (notification === Notification.resendVerifySignup) {
// Execute something...
return;
}
if (notification === Notification.emailConfirmed) {
// Execute something...
return;
}
}
But as you have to see, both have the same drawback. Now that we've come across the problem head-on. How do we improve this?
Solution
As in any software solution there are different implementations of how to solve a problem. In this case we are only going to address two, one of them is the use of objects
and the other is through mapping functions
Using objects
const notificationDirectory = {
verifySignup: () => {
// Execute something...
},
resendVerifySignup: () => {
// Execute something...
},
emailConfirmed: () => {
// Execute something...
}
}
type NotificationTypes = keyof typeof notificationDirectory
function notifier (notification: NotificationTypes) {
const handler = notificationDirectory[notification]
if (!handler) throw new Error("Your method does not exist");
if (!(typeof handler === 'function')) throw new Error("Your method must be a function");
// Execute your method
handler()
}
// send email for the user to confirm his email.
notifier('verifySignup')
// send email notifying that your email was verified
notifier('emailConfirmed')
Using Mapping Funtions
const notificationMap = new Map<string, () => void>()
notificationMap.set('verifySignup', () => {
// ...
})
notificationMap.set('resendVerifySignup', () => {
// ...
})
notificationMap.set('emailConfirmed', () => {
// ...
})
function notifier (notification: string) {
const handler = notificationMap.get(notification)
if (!handler) throw new Error("Your method does not exist");
handler()
}
The following details the advantages of using these techniques instead of switch:
Easier to maintain: As new notifications are added, there is no need to add more instances inside a switch statement, just add a new function to the
notificationDirectory
object.More flexible: you can add or remove notifications at any time without having to worry about updating the corresponding switch statement.
Safer: By using the
NotificationTypes
type, you can ensure that only valid values are passed to the notifier method. If an attempt is made to pass a value that is not one of the valid keys of thenotificationDirectory
object, a runtime error will be thrown. This can help catch bugs earlier.More scalable: Instead of writing a single long function that handles multiple different cases, this approach allows you to define a separate function for each case, making your code more modular and easier to read and maintain.
More efficient: In some cases, using a map instead of a switch statement can be more efficient in terms of execution speed. This is because maps are implemented as a key-value lookup data structure, which allows you to look up and retrieve values more efficiently than sequential lookup in a switch statement.
Conclusion
In conclusion, although the switch statement is a useful tool in software development, its excessive use can lead to code that is difficult to maintain and modify. As the number of cases within the switch increases, the readability of the code suffers, and adding new cases can lead to errors if you forget to update all the instances in which it is used. In addition, there are more scalable and maintainable alternatives, such as using objects
or mapping functions
, which can improve code readability and maintainability. Therefore, it is important to consider the proper use of the switch statement and evaluate other alternatives to improve code quality in software development.
Follow me
- 🐙 GitHub: https://github.com/thebug404
- 🐦 Twitter: https://twitter.com/thebug404
Top comments (49)
Imho this article is just creating a problem out of absolute nothing.
A switch statement is totally fine to use until it reaches a distinct amount of switch cases.
After that a refactoring would be necessary to maintain readability.
This would also be the case whe using if statements.
So just clickbait?
Here the original article source:
dev.to/tauantcamargo/replace-your-...
The decision to use or avoid switch statements depends on the specific situation and the developer's preferences and coding standards.Thanks for sharing
Totally agree, sometimes we just need guard clauses to make the code more readable.
In my personal opinion, the switch-case block in the first code block is perfectly fine and in my opinion also better than the presented alternatives. It is much more readable due to less code and probably also faster and more efficient than the other solutions.
The if-else option has to evaluate the condition three times in the worst case, instead of once. Not a big deal, to be honest, but if we are discussing such a topic, it needs to be mentioned.
The key-value option (object or map) contains much more boilerplate code, which makes it harder to read, understand, and maintain. It also causes memory overhead due to additional objects (map, functions, etc.).
That being said, I would extract the three notification functions into three separate functions and call them from either the switch block or the if block. No need for overengineering.
It certainly makes sense to avoid switch blocks in certain cases, especially if they span long lines of code. However, this is not such a case. And if the post title has such a strong opinion about switch-blocks, it should also present a strong use case against them.
But there is a tipping point. The example code is short for article readability. If there were 20, 200 or 2000 notifications the switch pattern is unmanageable (and untestable). However if you start with the switch pattern, the cost to refactor can be too great to fit into a development sprint and you end up with tech debt
Please, think about it. That's nonsens. I don't believe, you had such this situation in any of your sources. Not to manags and no way to test it? What? I use switch-blocks since over 40 years. And i would never use an if-then-else-basic-shit. Sorry. This article is redudant.
canonical example. medical SNOMED/HL7 codes. There are literally thousands of them... It is likely that a solution starts out for one particular medical diclipline and as the software expands, you need to do different things. In the end you need to be able to map an enumerated value to an action. Whether you do this with SWITCH, IF-ELSE, object keys or a database is an implementation detail.
You wouldn't be calling a different action for those codes, you'd be calling the same action with different parameters.
A more realistic case would be a device controller in which what you do depends on how the device answers back. Even then usually it's enough for a switch statement but some devices can be hella complex in their interfaces.
How would the condition to check which type of notification to send be any different? The notification type will be determined by either a) switch-statement, b) an if-statement or c) the key-value-map.
In all three cases you should most likely extract the actual notification logic into its separate function, depending on the length and complexity. But that has nothing to do with switch-statement in particular. It equally applies to all three use cases.
If you use TypeScript, you don't need to refactor your Switch statements. You can make them safer with this trick:
When you use type
never
TypeScript explicitly checks thatdefault
case is unreachable and shows you an error if it isn't.mejor usar un enum
Strongly disagree.
1) I don't think switches are less readable when you are used to them. They don't come up very often in code because they are only useful in specific cases so they might look "odd" to the inexperienced eye.
2) You haven't mentioned that switches are powerful when mutliple cases lead to the same branch.
3) Switch cases are NOT tested sequentially like a chain of if {} else if {}. Try to write a little piece of C code with a switch, compile it, look at the generated Assembly result and it might surprise you how good compilers are at optimising switches.
4) When it comes to performance, using a string as a case value is already a bad idea. A properly designed software should use enums for that anyway (in JS too). Besides, hashmaps are dynamic runtime structures, so they are not as predictable and therefore not as optimisable as the code branches of a switch, and that's true for any language.
The only thing I would agree on is that switches are often used to patch bad design, which might also be why they have a bad reputation. If I have a lot of branches in a switch, I would try to refactor the code using polymorphism.
Switches in JS are indeed sequential tests. Unless there are some JIT optimizations I'm not aware of, each case of a switch is tested in order until one matches.
I just had a quick look at the V8 source and it turns out that some switches are already optimisable by the JIT compiler before any heavy optimization work by TurboFan.
Specifically, any switch that meets the following conditions will get optimised with a jump table:
Here's the source code for that:
src/interpreter/bytecode-generator.cc
src/flags/flag-definitions.h
Note that the jump table will be tested first. If the switch has any non Smi cases, they will be tested AFTER any case available in the jump table, even if they appear before some of these cases in the code.
Also note that this is just a basic compilation optimisation. When the function becomes "hot" it will be picked up by the optimiser (TurboFan in V8). And the optimisation performed will be much more important than a simple jump table. Some of function calls inside the cases may even be inlined inside the parent function. It's much harder (though not impossible) for the optimiser to optimise if you use a hashmap because a hashmap is dynamic so the optimiser has to make assumption before inlining and verify that these assumptions are still true when calling the function.
I literally created an account with the sole intention of asking you how the **** did you come up with this code? Like, really, how do you know your way around this repo so good to find this? Or if not how did you manage to do it anyway? This is an invaluable skill imo
Thanks asaf!
I'm not familiar with the v8 source code but I could navigate it rather easily because:
Beyond that, it's a matter of opening and checking folders/files and guessing if you're on the right track. Also I have only dug into the interpreter code, which is the easy part I suppose.
All JS JIT compilers come with optimisers that will sort this out (e.g. V8's TurboFan).
Partially agree with you, although unsure about the proposed solution... In most cases I would just encourage people to use guard clauses, with clear descriptions like:
The use of
Objects
orMapping Functions
depends on the context of what is being integrated. It is up to the developer if it is useful or not. Thank you very much for your comment. 🤜I would take a switch statement over the provided alternatives any day. We need to keep readability in mind. When you return to that specific section in three years, you want to see at first glance what's happening.
In react I using switch-return for useReducer, I think that way much simplier, than create lot of extra function.
for example:
You don't need a switch at all for such a reducer, this is plenty:
that is only complicated the whole reducer, because in this case - real exmaple - you can call this "simpleReducer" from each line.
Switch version do not need to create 14 different reducer function call - one calls level less solution. Check this solution, in each case you declare which state property chagened. Plus as you saw this gameReducer have return type definition ( at once ), that guard each of them.
Nope, no need to declare separate functions
You only need advanced reducers if you have to manage arrays, like adding, updating or deleting items to/from array.
Have a look at immer. It might help you here
All this hassle can be avoided just by adding a "default:" with an error throw. If you forget to add the new fruit to the switch, it will throw an error exactly the same as your object solution.
Also, if the input comes from "outside" (not fully under your control), your object solution is bugged. Use "toString" as the notification name and see. Trying to find a safer solution you just created an unsafer one. Sometimes overengineering is a problem more than a solution.
And a default case is not even needed, check this example:
they kinda seem harder to read
I agree. I've used this pattern before, but with some refactoring.
I find this helps to keep everything in one place while separating them too.