4

Stop Using Switch in TypeScript — 3 Alternatives To Use Instead

 3 years ago
source link: https://betterprogramming.pub/stop-using-switch-in-typescript-3-alternatives-to-use-instead-aef014c9b31d
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Stop Using Switch in TypeScript — 3 Alternatives To Use Instead

You can avoid the switch statement, as TypeScript has its own versions

Man looking inside vending machine
Man looking inside vending machine
Photo by Victoriano Izquierdo on Unsplash.

What’s wrong with the following code?

You can very well say “nothing.” That would be a valid answer. But this code can be considered a code smell if you find it as part of a larger code base where there is a lot more logic around and inside it. Truth be told, switch statements aren’t that great.

With a switch statement, you’re creating a huge block of code that has to be executed serially, checking one condition after the next one. Its syntax allows for problems such as forgetting to write a break clause and causing multiple cases to be executed accidentally. You don’t want to use a switch statement if you can avoid it, but what are the alternatives?

Depending on the language you’re using, you’ll have different options at your disposal. Given that we’re dealing with TypeScript (and therefore, JavaScript), we can mix and match. Here are my three favorite ways of avoiding switch statements whenever I’m coding.

Using Object Literals

This is probably my favorite way of the three because it’s so simple and completely native to JavaScript and TypeScript. You don’t need any extra knowledge, libraries, or complex design patterns. All you need is the most basic construct: an object literal ({}).

The key to this solution is to encapsulate the logic inside each case block into an individual function and then index them in an object literal. That’s right, “index them.” Our object literal will act as an index for these code blocks and the lookup time will become O(1), which is a great added bonus.

Here is what I mean:

Granted, the complexity here is trivial, but you get the point. I’ve managed to encapsulate the logic for each case statement inside one method for each one. This already is a good practice, but then I removed the need for the whole switch construct by creating theIndex, which is just an object literal.

The cognitive load required to understand this code is smaller than going through a large switch . The lookup time required to find the right code to execute is smaller as well since the engine is not having to validate every single case. Instead, it can know exactly which one to look for.

And I know what you’re thinking: What if you need a default case for those unknown values? It’s as easy as adding an IF. You can even do it like this:

Taking Advantage of Polymorphism

If you’re a big OOP fan, this one might be more up your alley.

In our problem, we’re trying to encapsulate different versions of the same conceptual behavior: making our “animal” speak. They can go “woof,” “meow,” “quack,” or whatever we need them to, and while that might require different code, they’re conceptually doing the same thing.

So instead of having one method for each one, we might have the same method for all of them. The only difference is we’re creating different classes.

This allows us to specialize our logic inside each different class:

Our problem now? We need to treat each object differently. We can’t have a single list of animals and decide which method to call — unless, of course, we make sure they’re the same type. We can do that through a higher-order class (i.e. by using inheritance) or a common interface.

If our animals each had some common behavior that required repeated code, a common parent class would make sense. In our case, though, a simple common interface is more than enough:

Notice a few things:

  • Our interface is declaring a common shape for all our classes (the required speak method).
  • All our classes are now implementing the same IAnimal interface.
  • We can now forget about which animal we’re dealing with and simply ask it to speak.

That last one is the main benefit here. We no longer care about checking for the right animal. All we have to do is create the right type of animal and then the logic that would’ve originally used a switch statement to check for the right type is no longer required.

This is a very elegant way of solving the problem — especially if OOP is part of the rest of your code base.

Generics

Imagine trying to solve your switch-related problems with the Factory method only to realize you need a big switch statement to solve it. That’d be ironic, wouldn’t it? And not in the Alanis Morissette type of way. You know, the actual meaning of the word.

Pretend you’ve coded your first solution and you’ve cornered yourself into the following situation:

We can do better thanks to generics.

Generics are a way languages give you to represent types without actually specifying them. And TypeScript even allows you to use them to define function signatures. The solution to our problem requires two steps:

  1. Turn our switch-dependent function into a generic one that instantiates any type of animal we give it without needing a switch statement.
  2. Find a way to specialize that generic factory in such a way that we don’t need to be bothered with adding logic to decide which type of animal we’re building.

The second step is essentially the whole point of the switch statement, and if we can do away with it, then we don’t need it anymore.

The first step is solved by turning our animalCreator into this:

This function takes a “type” as the first parameter — one that, in fact, is a class (notice the ability to call the new method is required) — and then a bunch of generic attributes (i.e. the ...args part, also known as rest attributes).

Now that could be enough, but when using it, you’d have to do things like:

They work, but in the second example, we’re creating a dog that has nine lives (actually, that parameter would be ignored, but you get the point). Our code is not able to “force” the correct type.

So we can go a step further and create a generic type for our generator function — one we can apply and specialize. For instance, if we wanted to create a function that could only create dogs but reuses the same generic code, we could do the following:

And now we can do:

By now, we’ve removed the need for the switch statement successfully. You don’t need to have a big set of cases to determine which class to instantiate. We’re now directly passing in the class we want. But that’s not enough — not when we’re dealing with JavaScript and TypeScript — because we can go one step further and remove the redundant Dog parameter from the dogMaker. We’re already pretty sure we’re making a dog. We don’t need to also pass it as a parameter. It also removes the direct dependency of that class from our code.

To do that, we can use currying, a functional programming technique that involves creating a wrapper function that will preset the parameters we want and allow for the rest to be passed to the function. For our example, we want a dogMaker, which means it shouldn’t be the developer’s responsibility to pass it as a parameter. Here is the final result:

While it might seem like the most complex of the three options, if you’re trying to implement the Factory pattern, this might be a way to specialize your code even more and make it much easier to understand.

Conclusion

Fact: The switch statement is not the best solution for many cases. Another fact: While there might be some situations where it makes total sense, 90% of the time, there is a more declarative way to do it.

In this article, I’ve shared three approaches with you.

So what do you think? Are you still thinking about using a switch or have I changed your mind?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK