DEV Community

Alessio Michelini
Alessio Michelini

Posted on

Using Object Literals Instead of Switch Statements in JavaScript

Disclaimer
AI was used to create parts of this article and create some of the examples

When writing JavaScript, there are often multiple ways to solve the same problem. One common scenario is implementing conditional logic based on different values. The classic approach is to use a switch statement, but there's a more elegant solution using object literals that I've found to be cleaner and more maintainable in many situations.

The classic switch statement

First, let's remind ourselves how a typical switch statement looks:

const getAnimalSound = animal => {
  switch (animal) {
    case 'dog':
      return 'woof';
    case 'cat':
      return 'meow';
    case 'cow':
      return 'moo';
    case 'fox':
      return 'what does the fox say?';
    default:
      return 'unknown sound';
  }
};

console.log(getAnimalSound('dog')); // 'woof'
console.log(getAnimalSound('fox')); // 'what does the fox say?'
console.log(getAnimalSound('tiger')); // 'unknown sound'
Enter fullscreen mode Exit fullscreen mode

While this works fine, it's quite verbose and requires a lot of case statements, break statements (though we've avoided them here by using return), and it's not very DRY (Don't Repeat Yourself).

Enter object literals

Let's refactor the above function using an object literal:

const getAnimalSound = animal => {
  const sounds = {
    dog: 'woof',
    cat: 'meow',
    cow: 'moo',
    fox: 'what does the fox say?',
    default: 'unknown sound'
  };

  return sounds[animal] || sounds.default;
};

console.log(getAnimalSound('dog')); // 'woof'
console.log(getAnimalSound('fox')); // 'what does the fox say?'
console.log(getAnimalSound('tiger')); // 'unknown sound'
Enter fullscreen mode Exit fullscreen mode

The result is the same, but look at how much cleaner and more concise the code is! We've defined an object where each key is a potential value of our animal parameter, and each value is the corresponding sound.

We then use the parameter to access the corresponding property in our object. The || operator provides a fallback if the property doesn't exist (just like our default case in the switch).

Taking it further with functions

Object literals really shine when you need to do more than just return a simple value. Let's say we have a more complex example where we need to perform different operations based on a command:

const executeCommand = command => {
  switch (command) {
    case 'greet':
      console.log('Hello there!');
      break;
    case 'farewell':
      console.log('Goodbye!');
      break;
    case 'time':
      console.log(`Current time: ${new Date().toLocaleTimeString()}`);
      break;
    default:
      console.log('Unknown command');
  }
};

executeCommand('greet'); // 'Hello there!'
executeCommand('time'); // 'Current time: 12:34:56'
Enter fullscreen mode Exit fullscreen mode

We can refactor this using an object literal with functions as values:

const executeCommand = command => {
  const commands = {
    greet: () => console.log('Hello there!'),
    farewell: () => console.log('Goodbye!'),
    time: () => console.log(`Current time: ${new Date().toLocaleTimeString()}`),
    default: () => console.log('Unknown command')
  };

  // Execute the command if it exists, otherwise execute the default command
  (commands[command] || commands.default)();
};

executeCommand('greet'); // 'Hello there!'
executeCommand('time'); // 'Current time: 12:34:56'
Enter fullscreen mode Exit fullscreen mode

Again, we've reduced verbosity and made the code more maintainable. Each command is clearly associated with its implementation, and there's no need for break statements.

More complex examples

You can also handle more complex cases, where you might need to access the context or pass parameters to the functions:

const calculator = (a, b, operation) => {
  const operations = {
    add: (x, y) => x + y,
    subtract: (x, y) => x - y,
    multiply: (x, y) => x * y,
    divide: (x, y) => y !== 0 ? x / y : 'Cannot divide by zero',
    default: () => 'Unknown operation'
  };

  return (operations[operation] || operations.default)(a, b);
};

console.log(calculator(5, 3, 'add')); // 8
console.log(calculator(5, 3, 'multiply')); // 15
console.log(calculator(5, 0, 'divide')); // 'Cannot divide by zero'
console.log(calculator(5, 3, 'power')); // 'Unknown operation'
Enter fullscreen mode Exit fullscreen mode

In this case, we're passing the a and b parameters to whichever operation function is selected.

When to use object literals vs switch

While object literals offer a cleaner syntax in many cases, there are situations where a switch statement might still be preferable:

  1. When your case conditions aren't simple matches (e.g., they involve ranges or complex expressions)
  2. When you need to "fall through" multiple cases intentionally
  3. When your cases need access to variables in the surrounding scope

But for simple mapping between inputs and outputs or actions, object literals often provide a more elegant solution.

A practical real-world example

Let's say you're building a simple state machine for a UI component, where you need to handle different actions based on the current state:

const handleStateTransition = (currentState, action) => {
  const stateTransitions = {
    idle: {
      start: 'loading',
      reset: 'idle'
    },
    loading: {
      success: 'loaded',
      error: 'error',
      cancel: 'idle'
    },
    loaded: {
      refresh: 'loading',
      clear: 'idle'
    },
    error: {
      retry: 'loading',
      clear: 'idle'
    }
  };

  return stateTransitions[currentState]?.[action] || currentState;
};

let state = 'idle';
state = handleStateTransition(state, 'start'); // 'loading'
state = handleStateTransition(state, 'success'); // 'loaded'
state = handleStateTransition(state, 'clear'); // 'idle'
state = handleStateTransition(state, 'invalidAction'); // 'idle' (unchanged)
Enter fullscreen mode Exit fullscreen mode

This implementation is much cleaner than a massive switch statement with nested conditions, and it makes the state machine's logic very clear and easy to modify.

Conclusions

Object literals provide a powerful alternative to switch statements in JavaScript, offering several advantages:

  1. More concise and readable code
  2. Better performance in many cases (browsers can optimize object lookups)
  3. Easier to maintain and extend
  4. No need for break statements or worrying about fall-through behavior

Next time you find yourself reaching for a switch statement, consider whether an object literal might make your code cleaner and more maintainable. It's a simple but effective technique that I use frequently in my JavaScript projects.

I hope this helps you write cleaner and more elegant code!

Top comments (6)

Collapse
 
dariomannu profile image
Dario Mannu

Totally my style, love that!

I would add, always remember to watch out for prototype attacks, as well.
Suppose someone makes a call like these:

getAnimalSound('prototype')
getAnimalSound('__proto__')
Enter fullscreen mode Exit fullscreen mode

These could give hackers access to a number of things if not handled properly, including changing keys, changing functionality, adding spy functions, etc...

You may want to consider using a Map instead of an Object, even though the Map may be slightly slower with small objects but would keep you safe.

const getAnimalSound = animal => {
  const sounds = new Map(Object.entries({
    dog: 'woof',
    cat: 'meow',
    cow: 'moo',
    fox: 'what does the fox say?',
    default: 'unknown sound'
  }));

  return sounds.get(animal) ?? sounds.get('default');
};
Enter fullscreen mode Exit fullscreen mode

Good article and good patterns, though.

Collapse
 
darkmavis1980 profile image
Alessio Michelini

A Map is a really nice approach actually!

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

While the change to object seems to be the same as the switch statement performance-wise, the function variant is consistently slower than the switch variant: Benchmark

There's a 5 to 6% loss consistently, so avoid doing this in hot paths.

Collapse
 
darkmavis1980 profile image
Alessio Michelini

That's true, you loose some performances, but does it matter?
Don't get me wrong, it is important to know what you lose, but there are always trade-offs.
You could make the same argument about for versus forEach or map, but what it makes the code more readable and reusable?
I think if you worry about losing 5/6% of performances, because you are using this loop hundreds of thousands of times, maybe you should ask yourself some of this questions:

  • Am I using the write language for the job?
  • Does my code needs to run these many times?
  • Should I optmize my code in other ways?

Because while synthetic benchmarks tell you a story, that story often doesn't happen in real life.
If you are running that many iterations on the client side, you are at risk to crash the browser.
If you are running that many iterations on the server side, there are better languages that can deal with crunching large datasets in a more efficient ways.
The reality is that if your code has to run 200k operations a seconds, you shouldn't rely on a single node to do that job, because then that 5% of loss is the last of your problems.
Again, yes these benchmarks are interesting, it can lead to you to decide to write code in one way or another, but it's not the only factors that you need to consider.

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

I did say "hot paths". Avoid it on hot paths. 5% on a hot path is disastrous.

Collapse
 
pengeszikra profile image
Peter Vivo

I am prefered the switch statement because don't need to use a extra step, even can use a multiple case for solution. But I prefered to using a single switch return in a function.
Object mapping is great if we combine those object instead a single declaration.

document.addEventListener("keydown",
  /** @type {(event:KeyboardEvent) => any} */
  (event) => {
    const {key} = event;
    switch (key) {
      case "c": return drag = !drag;

      case "n": return nextDay();
      case "z": return toggleUI();
      case "[": return selectSheet(-1);
      case "]": return selectSheet(+1);
      case "a": return tool.w = (+ tool.w - 0.1).toFixed(2);
      case "w": return tool.h = (+ tool.h - 0.1).toFixed(2);
      case "s": return tool.h = (+ tool.h + 0.1).toFixed(2);
      case "d": return tool.w = (+ tool.w + 0.1).toFixed(2);
      case "A": return tool.w = (+ tool.w - 1).toFixed(2);
      case "W": return tool.h = (+ tool.h - 1).toFixed(2);
      case "S": return tool.h = (+ tool.h + 1).toFixed(2);
      case "D": return tool.w = (+ tool.w + 1).toFixed(2);
      case "j": return tool.m ++;
      case "h": return tool.m --;
      case "k": return tool.n ++;
      case "l": return tool.n --;
      case "v": return storeSprite();

      case ",":
      case ";": return interactionToLeft();

      case " ":
      case "'": return interactionCallCard();

      case ".":
      case "\\": return interactionToRight();
    }
  }
);
Enter fullscreen mode Exit fullscreen mode

or another example is:

/** @type {(st:State) => Promise<keyof Phases>} */
export const gameLoop = async (st) => {
  switch (st.phase) {
    case "SETUP": {
      st.player.deck = st.player.baseDeck;
      st.quest.deck = st.quest.baseDeck;
      return st.phase = "READY";
    }

    case "READY": {
      await delay(2000);
      return st.phase = "PLAYER_DRAW";
    }

    case "PLAYER_DRAW": {
      if (st.player.deck.length + st.player.hand.length < 4) {
        await pickUpPlayerDrop(st);
        st.player.deck = [...st.player.deck, ...st.player.drop].sort(shuffle);
        await reshufflePlayerCardToDeck(st);
        st.player.drop = [];
      }
      while (st.player.hand.length < 4) {
        const crd = st.player.deck.pop();
        await dealToPlayer(st, crd);
        st.player.hand.push(crd);
      }
      return st.phase = "QUEST_DRAW_WITH_END_CHECK"
    }

   // ... more phase of game
Enter fullscreen mode Exit fullscreen mode

code fragments from: dev.to/pengeszikra/javascript-grea...