DEV Community

Cover image for Use an object instead of a switch
Guilherme de Andrade
Guilherme de Andrade

Posted on • Updated on

Use an object instead of a switch

Sometimes we can do a simple refactor and achieve a lot with it! The example that I'm gonna show was taken from a real project that has been working just fine for a long time.

Still, that doesn't mean we shouldn't take the initiative to improve simply because it's already working! However, we also need to be pragmatic and not fall into the perfectionism trap. Basically, we should find the sweet spot where the effort necessary is paid by its own results. 🕵️

I was working on a module that had a getMonth function which would return the translation key according to the given month:

const getMonth = (month: string) => {
  let translationKey = ''
  switch (month) {
    case 'January':
      translationKey = 'JANUARY_TRANSLATION_KEY'
      break
    case 'February':
      translationKey = 'FEBRUARY_TRANSLATION_KEY'
      break
    case 'March':
      translationKey = 'MARCH_TRANSLATION_KEY'
      break
    case 'April':
      translationKey = 'APRIL_TRANSLATION_KEY'
      break
    case 'May':
      translationKey = 'MAY_TRANSLATION_KEY'
      break
    case 'June':
      translationKey = 'JUNE_TRANSLATION_KEY'
      break
    case 'July':
      translationKey = 'JULY_TRANSLATION_KEY'
      break
    case 'August':
      translationKey = 'AUGUST_TRANSLATION_KEY'
      break
    case 'September':
      translationKey = 'SEPTEMBER_TRANSLATION_KEY'
      break
    case 'October':
      translationKey = 'OCTOBER_TRANSLATION_KEY'
      break
    case 'November':
      translationKey = 'NOVEMBER_TRANSLATION_KEY'
      break
    case 'December':
      translationKey = 'DECEMBER_TRANSLATION_KEY'
  }
  return translationKey
}
Enter fullscreen mode Exit fullscreen mode

In this case, it was clear to me what I would accomplish using an object instead of a switch statement:

  • readability
  • cognitive complexity (you can read more about it here)

Why an object? Well, if you take a closer look at what the getMonth function is doing, you realize that it's doing nothing but mapping keys to values, which is exactly what an object does! ✨

Therefore, a switch statement isn't needed at all. actually, it just makes the code less readable and increases its cognitive complexity. So, after refactoring:

type Month =
  | 'January'
  | 'February'
  | 'March'
  | 'April'
  | 'May'
  | 'June'
  | 'July'
  | 'August'
  | 'September'
  | 'October'
  | 'November'
  | 'December'

type Mapping = Record<Month, string>

const MONTH_TO_TRANSLATION_KEY: Mapping = {
  January: 'JANUARY_TRANSLATION_KEY',
  February: 'FEBRUARY_TRANSLATION_KEY',
  March: 'MARCH_TRANSLATION_KEY',
  April: 'APRIL_TRANSLATION_KEY',
  May: 'MAY_TRANSLATION_KEY',
  June: 'JUNE_TRANSLATION_KEY',
  July: 'JULY_TRANSLATION_KEY',
  August: 'AUGUST_TRANSLATION_KEY',
  September: 'SEPTEMBER_TRANSLATION_KEY',
  October: 'OCTOBER_TRANSLATION_KEY',
  November: 'NOVEMBER_TRANSLATION_KEY',
  December: 'DECEMBER_TRANSLATION_KEY',
}

const getMonth = (month: Month) => MONTH_TO_TRANSLATION_KEY[month]
Enter fullscreen mode Exit fullscreen mode

I created a repository with both versions and used the cognitive-complexity-ts package to have numbers to compare (a higher score means a more complex code):

the image shows that the switch version has score 1 while the object version has score 0

As expected, the usage of an object makes this code less complex and also more readable, since it's less verbose than the switch statement.

tl;dr: whenever we notice that the switch is doing nothing more than mapping keys to values, we should use an object instead 👌

Discussion (12)

Collapse
charlesfries profile image
Charles Fries

Nice article. I'll just add that unless MONTH_TO_TRANSLATION_KEY is a dynamic object that could be changed at some point, you could simplify this by using the keyof typeof keywords to pull the object keys directly from the object, so you don't need to explicitly define the keys:

const MONTH_TO_TRANSLATION_KEY = {
  January: 'JANUARY_TRANSLATION_KEY',
  February: 'FEBRUARY_TRANSLATION_KEY',
  March: 'MARCH_TRANSLATION_KEY',
  April: 'APRIL_TRANSLATION_KEY',
  May: 'MAY_TRANSLATION_KEY',
  June: 'JUNE_TRANSLATION_KEY',
  July: 'JULY_TRANSLATION_KEY',
  August: 'AUGUST_TRANSLATION_KEY',
  September: 'SEPTEMBER_TRANSLATION_KEY',
  October: 'OCTOBER_TRANSLATION_KEY',
  November: 'NOVEMBER_TRANSLATION_KEY',
  December: 'DECEMBER_TRANSLATION_KEY',
};

const getMonth = (month: keyof typeof MONTH_TO_TRANSLATION_KEY) => MONTH_TO_TRANSLATION_KEY[month];
Enter fullscreen mode Exit fullscreen mode
Collapse
peerreynders profile image
peerreynders • Edited on

It makes sense to add a const assertion:

const MONTH_TO_TRANSLATION_KEY = {
  January: 'JANUARY_TRANSLATION_KEY',
  February: 'FEBRUARY_TRANSLATION_KEY',
  March: 'MARCH_TRANSLATION_KEY',
  April: 'APRIL_TRANSLATION_KEY',
  May: 'MAY_TRANSLATION_KEY',
  June: 'JUNE_TRANSLATION_KEY',
  July: 'JULY_TRANSLATION_KEY',
  August: 'AUGUST_TRANSLATION_KEY',
  September: 'SEPTEMBER_TRANSLATION_KEY',
  October: 'OCTOBER_TRANSLATION_KEY',
  November: 'NOVEMBER_TRANSLATION_KEY',
  December: 'DECEMBER_TRANSLATION_KEY',
} as const;

type Month = keyof typeof MONTH_TO_TRANSLATION_KEY;
type TranslationKey = typeof MONTH_TO_TRANSLATION_KEY[Month];

const toTranslationKey: (m: Month) => TranslationKey = (month) =>
  MONTH_TO_TRANSLATION_KEY[month];
Enter fullscreen mode Exit fullscreen mode

Given that object keys can only be strings or symbols (numbers are coerced to strings) Maps are useful as well:

const MONTH_KEY_ENTRIES = [
  ['January', 'JANUARY_TRANSLATION_KEY'],
  ['February', 'FEBRUARY_TRANSLATION_KEY'],
  ['March', 'MARCH_TRANSLATION_KEY'],
  ['April', 'APRIL_TRANSLATION_KEY'],
  ['May', 'MAY_TRANSLATION_KEY'],
  ['June', 'JUNE_TRANSLATION_KEY'],
  ['July', 'JULY_TRANSLATION_KEY'],
  ['August', 'AUGUST_TRANSLATION_KEY'],
  ['September', 'SEPTEMBER_TRANSLATION_KEY'],
  ['October', 'OCTOBER_TRANSLATION_KEY'],
  ['November', 'NOVEMBER_TRANSLATION_KEY'],
  ['December', 'DECEMBER_TRANSLATION_KEY'],
] as const;

type EntryKey<T> = T extends readonly any[]
  ? T[0] extends undefined
    ? never
    : T[0]
  : never;

type EntryValue<T> = T extends readonly any[]
  ? T[1] extends undefined
    ? never
    : T[1]
  : never;

type MonthKeyEntries = typeof MONTH_KEY_ENTRIES[number];
type Month = EntryKey<MonthKeyEntries>;
type TranslationKey = EntryValue<MonthKeyEntries>;

const TranslationKeyMap = new Map<Month, TranslationKey>(MONTH_KEY_ENTRIES);

function toTranslationKey(month: Month): TranslationKey {
  const XKey = TranslationKeyMap.get(month);
  if (typeof XKey === 'undefined')
    throw new Error(`Missing month ${month} in TranslationKeyMap`);

  return XKey;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
guilhermedeandrade profile image
Guilherme de Andrade Author

Oh thanks a lot for the tip!

Collapse
akashkava profile image
Akash Kava • Edited on

Even if it isn’t just mapping you can also use methods in object to perform more logic per case.

 {
    async January(input) { 
    …
    }

     …
     async default(input) {
     …
     }
}

 await (months[month]
    ?? months.default)(input)
Enter fullscreen mode Exit fullscreen mode
Collapse
marpme profile image
marpme (Marvin)

I think I tend to see your use-cases here, but apperately this might have been a bad choice for replacing it with a map methodology.

  1. Switch-Case are generally understood even by all kind of software engineers (while loose object referencing is not that common in many languages)
  2. the switch could be refactored to make it less complex:
const getMonthTranslationKey = (month: Month) => {
  switch(month) {
    case Month.JANUARY: 
      return "JANUARY_KEY"
    case Month.FEBRUARY:
      // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

This example would definitely save you some congnitive effort understanding this piece of code by removing the variable and the late return statement.

Collapse
siddharthshyniben profile image
Siddharth

Nice post 👍. This method has way more uses than mapping keys to values tho. We could make the object keys functions for infinite possibilities (if it is needed)

Collapse
pierrewahlberg profile image
Pierre Vahlberg

I believe the "correct" solution is one step further, to use a map instead of a custom type of an object. It is effectively a set of key value pairs. But you could index it with whatever you like. The difference would be that the reader of code in the future easily can tell that a variable is intended to be a Map of key values whilst an object could be any object, stuffed with functions and what not. It makes more sense in a longer example or when variable naming is not as convenient.

Good article any how, the use case is spot on.

You could even stretch it to using functions as values in the map, would you need to do more things in any of the cases. Its still more readable than the switch 😊

Collapse
lukeshiru profile image
Luke Shiru • Edited on

Why use a Map? Map would be just a like plain object with extra steps for this scenario.

Collapse
omgimalexis profile image
Alexis Tyler

“Therefore, a switch statement isn't needed at all.”

A switch statement is more performant. But sure.

Collapse
lukeshiru profile image
Luke Shiru
Collapse
peerreynders profile image
peerreynders

SpiderMonkey (FireFox) seems to be consistently favouring object property lookup (not surprising as that is a core mechanism in JavaScript that needs to be performant).

However somewhere around size > 1000 things get less predictable with V8 (Chromium). Perhaps putting getObjectSwitch on the hot code path may give it the full Turbofan treatment at some point in time.

Thread Thread
lukeshiru profile image
Luke Shiru

Yup, with ~2000 in Chrome I'm getting a faster switch (even with some changes in your implementation). But not a significant difference from my PoV, compared to the readability we gain, and the far easier maintenance. YMMV.