Replace your endless 'IF-ELSE' or 'SWITCH' Javascript statements with maps

tq-bit - Apr 25 '23 - - Dev Community

Conditional statements are one of the first concepts you learn. They are convenient for controlling the flow of your code. And every program, from small scripts to large enterprise applications, uses them extensively.

Until that crucial moment, the codebase grows. And grows. Aaand grows. And then you encounter a behemoth unseen by your eyes before.

The problem: The if-else pyramid

I've seen code like this in a few codebases now. A verbose nightmare of if-else clauses, often spanning over several rows.

if(userRole === "admin" && isAuthorized) {
  // do something for admin
} else if (userRole === "user" && isAuthorized) {
  // do something for user
} else if (userRole === "admin" && !isAuthorized && color === "blue") {
  // do something for admin with their color set to blue
} else if (userRole === "user" && !isAuthorized && color === "red") {
  // ... you get the idea
}
Enter fullscreen mode Exit fullscreen mode

What seems to offer remedy are nested if-statements

if(userRole === "admin") {
  if(isAuthorized) {
    // Do something for auth admin
  } else {
    // Do something for unauth admin
  }
} else if (userRole === "user") {
  if(isAuthorized) {
    // Do something for auth admin
  } else {
    // Do something for unauth admin
  }
}
Enter fullscreen mode Exit fullscreen mode

But imagine these checks get more complex and more nested. You start having a hard time tracking what part of the flow control you are in.

if(userRole === "admin") {
  if(isAuthorized === true) {
    if(color === "red") {
      if(loginCount > 5) {
        // Do something for auth admin with color red with more than 5 logins
      } else {
        // Do something for auth admin with color red with less than 5 logins
      }
      // Do something for auth admin with color red
    } else if (color === "blue") {
      // Do something for auth admin with color blue
    }
  } else {
    // Do something for unauth admin
  }
} // I'll leave the else out here for readability
Enter fullscreen mode Exit fullscreen mode

The above code poses several problems at once

  • It's verbose and spaghettified. And imagine whole logic code snippets or functions being executed in each clause.
  • It's hard on the eyes. Especially for somebody who didn't write it
  • Logic is mixed up with the application's state, which can change during development and sometimes runtime

The proposed solution: Javascript (Hash)maps

Unwinding and refactoring these logical labyrinths is challenging. In the end, there is no way around verbosity. But we can tackle points two and three.

Introducing maps

Note while you could use the Javascript new Map() structure, we'll start easy by using a simple Javascript object.

Let's start by moving the conditionals of the above function into a new object - one by one:

Layer 1: User role

The first layer is defined by the role of a user. Valid keys are:

  • admin
  • user
const sequenceMap = {
  admin: {},
  user: {}
}
Enter fullscreen mode Exit fullscreen mode

Layer 2: User authorization

The second layer checks whether a user is authorized for the resource. Valid keys are

  • authorized
  • notAuthorized
const sequenceMap = {
  admin: {
    authorized: {}, 
    notAuthorized: {}
  },
  user: {
    authorized: {}, 
    notAuthorized: {}
  }
}
Enter fullscreen mode Exit fullscreen mode

Layer 3: Login count

Imagine for some reason, you want to check how often the user has logged in. Based on that, you execute different logic. Valid keys are:

  • loginCountUnderFive
  • loginCountOverFive
const sequenceMap = {
  admin: {
    authorized: {
      loginCountUnderFive: {}, 
      loginCountOverFive: {}
    }, 
    notAuthorized: {
      loginCountUnderFive: {}, 
      loginCountOverFive: {}
    }
  },
  user: {
    authorized: {
      loginCountUnderFive: {}, 
      loginCountOverFive: {}
    }, 
    notAuthorized: {
      loginCountUnderFive: {}, 
      loginCountOverFive: {}
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Layer 4: Color

On our final layer, we'll check the color of, well, maybe the user? Valid keys are:

  • red
  • blue
  • green
const sequenceMap = {
  admin: {
    authorized: {
      loginCountUnderFive: {
        red: {}, 
        blue: {}, 
        green: {}
      }, 
      loginCountOverFive: {
        red: {}, 
        blue: {}, 
        green: {}
      }
    }, 
    notAuthorized: {
      loginCountUnderFive: {
        red: {}, 
        blue: {}, 
        green: {}
      }, 
      loginCountOverFive: {
        red: {}, 
        blue: {}, 
        green: {}
      }
    }
  },
  user: {
    authorized: {
      loginCountUnderFive: {
        red: {}, 
        blue: {}, 
        green: {}
      }, 
      loginCountOverFive: {
        red: {}, 
        blue: {}, 
        green: {}
      }
    }, 
    notAuthorized: {
      loginCountUnderFive: {
        red: {}, 
        blue: {}, 
        green: {}
      }, 
      loginCountOverFive: {
        red: {}, 
        blue: {}, 
        green: {}
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Frankly, this doesn't look much better at first glance.
But we just created a literal roadmap of our control flow. Each segment inside the new object is unique and distinguishable.
A convenient side effect: VSCode will provide you with code completions when accessing the sequenceMap

Layer 5(ish): The logic

Let's add something our program can execute. For example a function for

  • an admin
  • who is authorized
  • has a loginCountOverFive
  • and a color of green

The segment of this map can be targetted using:
sequenceMap['admin']['authorized']['loginCountOverFile']['green']
The value of this segment will be located here:

const sequenceMap = {
  admin: {
    authorized: {
      /* ... */
      loginCountOverFive: {
        /* ... */
        green: {} // <-- here it is
      }
    }, 
    notAuthorized: { /* */ }
  },
  user: { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

You can now assign a variable or a function to it. Ideally, these will have the same name for each segment to avoid runtime errors.

In this case, we'll just assign a string and log it to the console.

const sequenceMap = {
  admin: {
    authorized: {
      /* ... */
      loginCountOverFive: {
        /* ... */
        green: "Hello admin. You are authorized, have over five login count. Also, you're green."
      }
    }, 
    notAuthorized: { /* */ }
  },
  user: { /* ... */ }
}

console.log(sequenceMap['admin']['authorized']['loginCountOverFive']['green'])
Enter fullscreen mode Exit fullscreen mode

And there we have it. A neat and convenient way to control your flow without if-else statements.

Optimizations

Before using the above in production, you might want to consider the following points:

  • Exception handling for missing segments
  • Encapsulation
  • Freezing the sequence map

Exception handling for missing segments

Trying to access a segment that does not exist will throw a type error. In classical if-else or switch statements, you would want a default value. Sometimes for each segment separately.

Handling missing segments is crucial for debugging the sequence map later!

If you need just a single fallback case for the whole map, you can use the ternary operator ? when accessing the sequence map.

console.log(sequenceMap['a']?.['b']?.['c']?.['d'] || "Role sequence not recognized")
Enter fullscreen mode Exit fullscreen mode

When you need a fallback case for every single segment, it gets tricky. In this case, since the final segment always returns a string, we could add a 'default' segment to each layer. And a function to evaluate our sequence map instead of trying to access it directly:

const sequenceMap = {
  admin: {
    authorized: {
      /* ... */
      loginCountOverFive: {
        /* ... */
        green: "Hello admin. You are authorized, have over five login count. Also, you're green.",
        default: "Color not recognized"
      }, 
      default: "Login count not recognized"
    }, 
    notAuthorized: { /* */ },
    default: "Authorization not recognized"
  },
  user: { /* ... */ },
  default: "User level not recognized"
}

function evalSequenceMap(userLevel, authorization, loginCount, color) {
  return !sequenceMap[userLevel] ? sequenceMap['default'] : 
  !sequenceMap[userLevel][authorization] ? sequenceMap[userLevel]['default'] : 
  !sequenceMap[userLevel][authorization][loginCount] ? sequenceMap[userLevel][authorization]['default'] : 
  !sequenceMap[userLevel][authorization][loginCount][color] ? sequenceMap[userLevel][authorization][loginCount]['default'] : sequenceMap[userLevel][authorization][loginCount][color]
}

console.log(evalSequenceMap('admin', 'authorized', 'loginCountOverFive', 'green'))
console.log(evalSequenceMap('admin', 'authorized', 'loginCountOverFive', 'blue'))
console.log(evalSequenceMap('admin', 'authorized', 'someCount', 'green'))
console.log(evalSequenceMap('admin', 'Someauth', 'loginCountOverFive', 'green'))
console.log(evalSequenceMap('developer', 'authorized', 'loginCountOverFive', 'green'))
Enter fullscreen mode Exit fullscreen mode

Encapsulation

Sequence maps like this can become massive. It makes sense to put each sequence map into a separate folder and export it as JSON or a Javascript object.
If you're using OOP, you could also create a public class that handles the state and can access its values in a convenient manner.

class Authorizer {
  constructor() {
    this.sequenceMap = {
      admin: {
        authorized: {
          /* ... */
          loginCountOverFive: {
            /* ... */
            green: "Hello admin. You are authorized, have over five login count. Also, you're green."
          }
        }, 
        notAuthorized: { /* */ }
      },
      user: { /* ... */ }
    }
  }

  getGreenAuthAdminWithManyLogins() {
    return this.sequenceMap['admin']['authorized']['loginCountOverFive']['green']
  }
}

console.log(new Authorizer().getGreenAuthAdminWithManyLogins())
Enter fullscreen mode Exit fullscreen mode

Freezing the sequence map

Javascript objects are extendable by default. Our sequence map is no different. This makes it prone to unexpected changes by other devs or malicious prototype pollution.
To prevent this, you can use Object.freeze(sequenceMap). It will cause possible changes to the object to silently fail.

Object.freeze(sequenceMap);

sequenceMap.admin = {
  admin:  "Hello"
}

console.log(sequenceMap['admin']['authorized']['loginCountOverFive']['green']) // <-- (still works)
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .