Telerik blogs

See how pattern matching is a more reliable solution for complex conditional statements than if-else.

Software is always evolving. Companies are constantly adding new features or removing obsolete ones. Over time, software requirements have become more dynamic. This is why code readability is such a hot topic these days.

Code readability is critical in how software is written today. It has many advantages, such as maintainability (the code becomes easier to maintain), scalability (we can add new features without any problems), collaboration (many developers can work and read the code), and so on.

It is critical that whenever we begin working on something new for an application, we use the best practices and code patterns. We should remember that many people in the future may read our code, so writing readable code should be a priority.

The question then becomes how to write readable code. What are the most important practices and code patterns that we can use to ensure the readability of our code? Aside from the well-known practices that improve code readability—code review, tests, documentation, meaningful names—we can employ some code patterns to make our code more readable.

Before we get into one specific code pattern that can help us with readability, let’s first explain the issue. We’re going to look at why if-else statements may sometimes be harmful to our code.

The Question of If-Else Statements and Readability

When we first start writing code, one of the first things we learn is how to write an if-else statement correctly. They are an excellent code for determining whether an expression is true or false.

var num = 1;

if (num === 1) {
 console.log("True :)");
}
else {
 console.log("False :(");
}

If-else statements are grand because of their simplicity. We can easily understand what is happening in our code without a deep understanding of programming.

The issue is that if-else statements can be problematic when misused. They can (surprisingly) become difficult to read and cause unexpected errors. When developers create deeply nested if-else statements, they can become difficult to read and unmaintainable, making the code poor.

Many programmers may wonder, “Should we avoid if-else statements?” The short answer is no, we should not. If-else statements aren’t wrong and are a great way to test expressions and eliminate some readability. They should be used for simple use cases.

Many JavaScript developers are unaware that there is a fantastic code pattern for writing conditional code: pattern matching. It is a code pattern that assists us in solving many of our problems when conditional branching is required. Don’t be alarmed by the term “conditional branching”—it may sound complicated, but it isn’t.

Let’s learn more about conditional branching, pattern matching, and how it can help us write code with fewer lines that are easier to read.

Pattern Matching

Pattern matching is a type of conditional branching, which is a programming mechanism that allows us to run specific pieces of code based on the evaluation of conditions. It will enable us to specify a situation and execute different parts of code based on whether the state is true or false.

“Well, I can do that with if-else or switch statements,” you may think, and you are correct! They are easy-to-use statements, making them a valuable and efficient tool for developers. The problem is that when we have to evaluate complex conditions, they are not a good idea. This is where pattern matching can help us.

Pattern matching is checking if a given value has the shape defined by a pattern. It involves comparing a value against a pattern, and, if a match is found, it takes some action, such as returning a value or executing a block of code.

Let’s learn by example. Imagine that we are going to receive an object, and this object has a property called “type”. The type property can have multiple values such as idle, loading, done, error, invalid, etc. Handling all that inside an if-else statement can become harder as our property changes values and we need to handle more cases, right?

A beginner developer would solve this problem using a if-else statement like this:

type DataType = {
  type: "idle" | "loading" | "done" | "error" | "invalid";
};

const obj: DataType = {
  type: "idle"
};

if (obj.type === "idle") {
  // ...
} else if (obj.type === "loading") {
  // ...
} else if (obj.type === "done") {
  // ...
} else if (obj.type === "error") {
  // ...
} else if (obj.type === "invalid") {
  // ...
}

This code is difficult to read and becomes increasingly complex over time. If we add more checks to our statement, we may end up with a huge mess. Consider how difficult it would be to handle if we had to create nested reviews within each check.

Solving this problem with pattern matching becomes far too simple! Unfortunately, pattern matching is not yet a feature of JavaScript/TypeScript; there is a TC39 proposal to add pattern matching to the ECMAScript specification. However, it may be a few years before we can use it on our code.

While pattern matching is not a feature of JavaScript, we can use some open-source packages for it, the best of which is ts-pattern. This TypeScript library allows us to pattern match in many different data structures with an expressive API, exhaustiveness checking support, ensuring that we match every possible case, and all of this in a small bundle size (only 1.7kB).

Using ts-pattern is simple; all we need to do is import the match function for each with that we have. We need to pass a pattern (the pattern against which we want our value to be checked) and a handler (a function that will be fired in case we have a match). Here’s an example using the ts-pattern:

import { match } from "ts-pattern";

type Result = {
  type: "idle" | "loading" | "done" | "error" | "invalid";
};

const result: Result = { type: "error" };

match(result)
  .with({ type: "idle" }, () => console.log("idle"))
  .with({ type: "error" }, () => console.log("error"))
  .with({ type: "done" }, () => console.log("error"))
  .exhaustive();

The exhaustive function is one of the best aspects of the ts-pattern. This function is in charge of carrying out the pattern matching expression and returning the result. It also allows us to check for exhaustiveness, ensuring that we have not ignored any possible case in our input value. This added level of security is very useful because forgetting a case is a common issue, especially in an evolving code base.

We haven’t tested all possible cases in our example, which is why TypeScript is warning us. Let’s fix this by checking all possible cases and returning a simple console.log for each one:

import { match } from "ts-pattern";

type Result = {
  type: "idle" | "loading" | "done" | "error" | "invalid";
};

const result: Result = { type: "error" };

match(result)
  .with({ type: "idle" }, () => console.log("idle"))
  .with({ type: "loading" }, () => console.log("loading"))
  .with({ type: "done" }, () => console.log("done"))
  .with({ type: "error" }, () => console.log("error"))
  .with({ type: "invalid" }, () => console.log("invalid"))
  .exhaustive();

Similar to default on switch statements, we can use the otherwise function which takes an handler function returning a default value. If we don’t want to use any of those functions, we can simply use run instead (it’s unsafe and might throw runtime error if no branch matches your input value).

Conclusion

Pattern matching can improve our code and make it easier to maintain and debug. It is a valuable code pattern we can use daily to make our jobs easier and create better applications. The ability to use pattern matching in TypeScript with ts-pattern is fantastic. We can combine the best of both worlds: type inference and code clarity and conciseness.


Leonardo Maldonado
About the Author

Leonardo Maldonado

Leonardo is a full-stack developer, working with everything React-related, and loves to write about React and GraphQL to help developers. He also created the 33 JavaScript Concepts.

Related Posts

Comments

Comments are disabled in preview mode.