DEV Community

Cover image for 🚫 Stop using SWITCH, please 🙏
Ivan Zaldivar
Ivan Zaldivar

Posted on

🚫 Stop using SWITCH, please 🙏

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 
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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 the notificationDirectory 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

Top comments (49)

Collapse
 
danielepecora profile image
Daniele Pecora • Edited

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-...

Collapse
 
wizdomtek profile image
Christopher Glikpo ⭐

The decision to use or avoid switch statements depends on the specific situation and the developer's preferences and coding standards.Thanks for sharing

Collapse
 
ivanzm123 profile image
Ivan Zaldivar

Totally agree, sometimes we just need guard clauses to make the code more readable.

Collapse
 
zirkelc profile image
Chris Cook • Edited

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.

Collapse
 
aarone4 profile image
Aaron Reese

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

Collapse
 
heinric14597733 profile image
Heinrick

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.

Thread Thread
 
aarone4 profile image
Aaron Reese

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.

Thread Thread
 
emi_black_ace profile image
Jacob Van Wagoner

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.

Collapse
 
zirkelc profile image
Chris Cook

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.

Collapse
 
suobig profile image
Popov Anton • Edited

If you use TypeScript, you don't need to refactor your Switch statements. You can make them safer with this trick:

type Color = 'red' | 'blue' | 'green'

export function preventSwitchDefault(_: never) {
  throw Error('this code should not be reached')
}

export function testColorSwitch(color: Color) {
  switch(color) {
    case 'red':
      return 'color is red'
    case 'blue':
      return 'color is blue'
    default:
      preventSwitchDefault(color)
  }
}
Enter fullscreen mode Exit fullscreen mode

When you use type never TypeScript explicitly checks that default case is unreachable and shows you an error if it isn't.

Collapse
 
salcrio profile image
Sergio

mejor usar un enum

Collapse
 
neeh profile image
neeh • Edited

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.

Collapse
 
joshuakb2 profile image
Joshua Baker

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.

Collapse
 
neeh profile image
neeh • Edited

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:

  • uses small integers as case values (Smi = 31 bits)
  • has at least 6 cases
  • has a spread (difference between the max and min case values) that is at most 3 times larger than the number of cases

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.

Thread Thread
 
asafalroy profile image
Asaf Alroy

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

Thread Thread
 
neeh profile image
neeh

Thanks asaf!
I'm not familiar with the v8 source code but I could navigate it rather easily because:

  • I have a rough idea of what I will find in the code: I know that there is an interpreter that converts JS to bytecode and an optimizer that optimizes this bytecode and I know that they must both be isolated from the rest of the code to limit the complexity of such a large codebase
  • The code uses predictable names for folders/files/functions/variables like "interpreter", "bytecode-generator.cc", "IsSwitchOptimizable", "IsSpreadAcceptable". It's almost impossible to find better names. Besides, the developers were generous enough to leave readable comments to clear any confusion that may arise when reading the code
  • I cloned the repo locally on a SSD so I could do folder-wide searches quickly

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.

Collapse
 
neeh profile image
neeh

All JS JIT compilers come with optimisers that will sort this out (e.g. V8's TurboFan).

Collapse
 
brense profile image
Rense Bakker

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:

const isUserSigningIn = true

if(isUserSigningIn){
  // Do sign-in stuff
  return
}

// Etc...
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ivanzm123 profile image
Ivan Zaldivar

The use of Objects or Mapping 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. 🤜

Collapse
 
jamesvanderpump profile image
James Vanderpump

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.

Collapse
 
pengeszikra profile image
Peter Vivo

In react I using switch-return for useReducer, I think that way much simplier, than create lot of extra function.

for example:

export const gameReducer = (state:MainState, {type, payload}):MainState => {
  switch (type) {
    case SET_HERO: return {...state, hero: payload};
    case MOD_HERO: return {...state, hero: payload(state.hero), combatResult:null};
    case NEXT_ROUND: return {...state, round: payload(state.round)};
    case SET_GAME_STATE: return {...state, game:payload};
    case SETUP_ENTITIES: return {...state, entities: payload};
    case SET_MOB_LIST: return {...state, mobList: payload};
    case MOD_ENTITI: {
      const {entities} = state;
      return entities[payload?.uid] 
        ? {...state, entities: {...entities, [payload?.uid]: payload} }
        : state;
    };
    case FOCUS_ON: return {...state, focus: payload};
    case PLAY_ACTION_ANIM: return {...state, actionAnim: payload};
    case PLAY_FLOW: return {...state, flow: payload};
    case ENCOUNTER_BEGIN: return {...state, encounterResult: null};
    case ENCOUNTER_RESULT: return {...state, encounterResult: payload};
    case SET_AUTO_FIGHT: return {...state, isAutoFight: payload};

    case LEVEL_UP_HERO: return {...state, hero: increaseLevel(1)(state.hero)}
    default: return state;
  }
};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
brense profile image
Rense Bakker

You don't need a switch at all for such a reducer, this is plenty:

function simpleReducer (prevState, nextState){
  return { ...prevState, ...nextState }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
pengeszikra profile image
Peter Vivo

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.

Thread Thread
 
brense profile image
Rense Bakker

Nope, no need to declare separate functions

const initialState = {
  user: { name: "Someone" },
  product: { title: "Something" }
}

function simpleReducer(prevState: typeof initialState, nextState: Partial<typeof initialState>){
  return { ...prevState, ...nextState }
}

function SomeComponent(){
  const [{ user, product }, setState] = useReducer(simpleReducer, initialState)

  const updateOneProperty = useCallback(() => {
    setState({ user: { name: "Someone else" }})
  }, [])

  const updateAll = useCallback(() => {
    setState({ user: { name: "Foobar" }, product: { title: "Hello world!" }})
  }, [])

  return <>
    <span>{user.name} - {product.title}</span>
    <button onClick={updateOneProperty}>Update one property</button>
    <button onClick={updateAll}>Update all</button>
  </>
}
Enter fullscreen mode Exit fullscreen mode

You only need advanced reducers if you have to manage arrays, like adding, updating or deleting items to/from array.

Collapse
 
bobbyconnolly profile image
Bobby Connolly

Have a look at immer. It might help you here

Collapse
 
llorx profile image
Jorge Fuentes

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.

Collapse
 
facum profile image
Facundo Montero • Edited

And a default case is not even needed, check this example:

export function notifier (notification: Notification) {
  switch (notification) {
    case Notification.verifySignUp: 
      // Execute something...
    return true;

    case Notification.resendVerifySignup: 
      // Execute something...
    return true;

    case Notification.emailConfirmed: 
      // Execute something...
    return true;
  }

  return false;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
husky931 profile image
Husky

they kinda seem harder to read

Collapse
 
ant_f_dev profile image
Anthony Fung

I agree. I've used this pattern before, but with some refactoring.

const verifySignup = () => {
  // ...
};

const resendVerifySignup = () => {
  // ...
};

const emailConfirmed = () => {
  // ...
};

const notificationMap = new Map<string, () => void>([
  ['verifySignup', verifySignup],
  ['resendVerifySignup', resendVerifySignup],
  ['emailConfirmed', emailConfirmed]
]);
Enter fullscreen mode Exit fullscreen mode

I find this helps to keep everything in one place while separating them too.