Search code examples
typescriptenumsswitch-statementreact-typescript

Can you increment/decrement over a TypeScript Enum to set the condition for a switch statement?


TL;DR: Can you use a TypeScript enum to set a switch statement condition, using onClick functions to move up and down the enum, and keep the result constrained within that enum?

On a project, I needed to control which React component would display in a multi-level menu. The menu options were each objects fetched from an API, with some properties being arrays of other objects. For instance, on the first level, you would select a "Type", which would then return the second level populated with "Subtypes" associated with that "Type"; then you would click on the "Subtype" and you would get a list of "Subtype-Items," etc.

I was advised to use a switch statement to control this menu, and it worked. But I thought I could make it more efficient by using a Typescript enum to set the condition. Something like:

enum MenuLevel {
   TYPE,
   SUBTYPE,
   SUBTYPEITEM,
   ITEMOPTION
}

const displayLevel = (currentLevel: MenuLevel) => {
   switch (currentLevel) {
     case "TYPE": // or case 0:
       return <TypeMenu/>
     case "SUBTYPE":
       return <SubTypeMenu/>
   //  etc., etc., etc.,
     

You get the idea. My thinking went that since TypeScript enums - as long as you don't initialize them with strings - have numeric values for each, I could have a function like the following to automatically progress to the next level:

function progress(currentLevel: MenuLevel) {
  setCurrentLevel(currentLevel + 1); // CurrentLevel being set in state
}

I tried a few different ways about this. I tried passing MenuLevel.1, or MenuLevel.TYPE. I tried initializing it with numbers, but no matter what I did it wasn't passing what I needed to the switch statement. I concluded that I just didn't know enough about TypeScript enums to make this work and abandoned it, going back to simpler methods.

It still bugged me, though, and so I experimented on Stackblitz to try and figure out if this was possible. I followed this StackOverflow answer by jonrsharpe as one example, but like my other experiments in this dev environment, it didn't work. I always get the number back, but it isn't constrained within the enum, which only has 3-4 members. It would even go into negative numbers if you decremented too much! Obviously that would end up breaking the switch statement and the displayLevel() function. So now I'm wondering, do I just lack proper understanding of enums, or is this truly impossible with them? Has anyone else tried anything like this?


Solution

  • So, I don't know if this will be useful to anyone, but on the off chance it is, here is what I found from playing around with TypeScript enums. I think I found a way to use them for switch statements and control logic. (Examples below are in React using TypeScript.)

    So, as jonrsharpe explicitly explained, incrementing or decrementing a number does not automatically constrain it to the values inside the enum. You have to provide control logic for that yourself. I also noticed, however, that it also did not work if you used a numeric array. Before we get to enums, consider the array case.

    export default function CurrentNumber() {
      const numbers = [1, 2, 3, 4, 5] as const;
      type OurNumber = typeof numbers[number];
      const [currentNumber, setCurrentNumber] = useState<OurNumber>(numbers[0]);
    
      const min = numbers[0];
      const max = numbers[numbers.length - 1];
    
      function plusNum(currentNumber: OurNumber) {
        if (currentNumber < max) {
          let plusNumber = Number(currentNumber);
          plusNumber = plusNumber + 1;
          const newNumber: OurNumber = plusNumber as OurNumber;
          setCurrentNumber(newNumber);
        } else {
          console.error("Max number already reached");
        }
      }
    
      function minusNum(currentNumber: OurNumber) {
        if (currentNumber > min) {
          let minusNumber = Number(currentNumber);
          minusNumber = minusNumber - 1;
          const newNumber: OurNumber = minusNumber as OurNumber;
          setCurrentNumber(newNumber);
        } else {
          console.error("Min number already reached");
        }
      }
    
      return (
        <div>
          <p>Current Number is: {currentNumber}</p>
          <div style={{ display: "flex", justifyContent: "space-around" }}>
            <button
              className="button is-primary"
              onClick={() => plusNum(currentNumber)}
            >
              Increment
            </button>
            <button
              className="button is-primary"
              onClick={() => minusNum(currentNumber)}
            >
              Decrement
            </button>
          </div>
        </div>
      );
    }
    

    We start by defining an array of numbers, and then we create a type from that array. (I found out this was a thing by reading Steve Holgado's post.) We assert the array as const so it is not just the numbers but the values themselves (Holgado did this with an array of strings, it may be unnecessary for an array of numbers.) Creating this type, and then making our counter variable of this type, will keep us in bounds. With the plusNum and minusNum functions, we still have to check if the counter is at the min or max of our array. If its not and somewhere in between, then we have to convert it to a regular number. Being typeof OurNumber, our currentNumber variable is actually currentNumber: 2 | 1 | 4 | 3 | 5 -- being as it is an array, it's not even in proper order. (Or maybe my VS Code has a cold.) Adding or subtracting from it will throw an error. So, we use Number(currentNumber), then do the math, then reconvert it back to OurNumber, and in React we use setCurrentNumber to set that in state. As you can see, I don't think this is terribly useful, and is probably a lot of extra work.

    Now, on to a TypeScript enum, let's say we're using it to control the stages of a task or publication in production, and we have the following:

    enum ECurrentStage {
      "START" = 1,
      "MIDDLE" = 2,
      "FINAL" = 3,
      "REVIEW" = 4,
    }
    
    export default function CurrentStage() {
      const [CurrentStage, setCurrentStage] = useState<ECurrentStage>(
        ECurrentStage.START
      );
    
      function action(value: ECurrentStage, action: "increment" | "decrement") {
        switch (action) {
          case "increment":
            value++;
            if (value > 4) {
              return TypeError("currentStage is out of bounds");
            } else {
              setCurrentStage(value);
            }
            break;
          case "decrement":
            value--;
            if (value < 1) {
              return TypeError("currentStage is out of bounds");
            } else {
              setCurrentStage(value);
            }
            break;
          default:
            break;
        }
      }
    
      return (
        <div>
          <p>Current Stage Is: {ECurrentStage[CurrentStage]}</p>
          <div style={{ display: "flex", justifyContent: "space-around" }}>
            <button
              className="button is-primary"
              onClick={() => action(CurrentStage, "increment")}
            >
              Increment
            </button>
            <button
              className="button is-primary"
              onClick={() => action(CurrentStage, "decrement")}
            >
              Decrement
            </button>
          </div>
        </div>
      );
    }
    
    

    Now, we can increment or decrement the value directly. Here, value is the numeric portion of this enum. We still have to check if it is outside of our enum's bounds, and if so, throw some kind of Error. If not, we can then just set the CurrentStage state variable directly. Then, if we want the word and not the number -- which in my case is how the switch statement in my project was set up -- we use object bracket notation and set ECurrentStage[CurrentStage] and there we have it. In this example I'm just displaying it on a page, but this could easily go into switch (ECurrentStage[CurrentStage]) {} for managing logic elsewhere.

    I'm really not sure if any of this is useful, but I didn't want my question to remain unanswered in case someone in the future has a similar idea and Google leads them here. These are definitely not the most efficient or elegant code examples either, and I'm certain they can be significantly improved upon. But hopefully this helps a future codeslinger implement their control logic more quickly and efficiently.