DEV Community

Cover image for Simplifying JavaScript Development with TS-Pattern and Pattern Matching
Matías Hernández Arellano
Matías Hernández Arellano

Posted on • Originally published at matiashernandez.dev

Simplifying JavaScript Development with TS-Pattern and Pattern Matching

Este artículo fue originalmente escrito en https://matiashernandez.dev

Have you ever wished for better pattern matching in JavaScript? Look no further! We have a game changer for TypeScript developers - TS-Pattern. This powerful library simplifies pattern matching and type checking in TypeScript, allowing you to create cleaner, more readable and maintainable code. In today's post, we will explore TS-Pattern by creating a reducer function to be used with the useReducer hook. But, before diving into the code, make sure to check out the TS-Pattern library on GitHub and give the repository a star!

Pattern matching is a powerful feature commonly found in functional programming languages. It allows you to test a value against a set of patterns (usually defined through algebraic data types) and execute different code blocks based on the matched pattern. With this feature you can simplify the code and makes it more declarative and intuitive to read, write and maintain.

Sadly JavaScript don't have this feature as part of the languge, but you can still use it through the addition of libraries such as TS-Pattern.

Currently there is a proposal to add pattern matching, but is still in stage 1 at TC39

Overview of TS-Pattern

TS-Pattern is a library that brings pattern matching and full type safety support to your TypeScript code. The primary goal is to transform your code into a pattern-matching type of code that is fully type-safe and with type inference. Check out the TS-Pattern GitHub repository to learn more, and make sure to give GitHub user Gabriel Vernal a follow on Twitter.

Getting Started with TS-Pattern

To demonstrate how TS-Pattern works, let's create a reducer function that can be used with the useReducer hook within a React component. First, let's import the necessary libraries:

You can install ts-pattern directly from npm


import React from 'react';
import { match } from 'ts-pattern';

Enter fullscreen mode Exit fullscreen mode

Next, create a state type to hold the following (example) information:

  • editing: a boolean
  • modals: an object with two boolean properties, a and b
  • data: a Record<string, unknown> type

For example:


type State = {
  editing: boolean;
  modals: {
    a: boolean;
    b: boolean;
  };
  data: Record<string, unknown>;
};

Enter fullscreen mode Exit fullscreen mode

Now, define a union type for the possible action types:


type ActionTypes =
  | 'toggleEditing'
  | 'enableEditing'
  | 'disableEditing'
  | 'toggleModelA'
  | 'toggleModelB'
  | 'updateData';

Enter fullscreen mode Exit fullscreen mode

Building Actions

Usually, when creating the list of possible actions that can be used with the useReducer hook you write a thing like this


type Actions = 
    | { type: "toggleEditing" }
    | { type: "toggleModalA", payload: { id: number {
Enter fullscreen mode Exit fullscreen mode

And you repeat that as many times as actions types you have, but it can be tedious and prone to error, so since we already have the actions as a separate union, let's use that to create an utility type to generate the actions


type CreateAction<T extends ActionTypes, P = undefined> = P extends undefined
  ? { type: T }
  : { type: T; payload: P };

Enter fullscreen mode Exit fullscreen mode

This utility type accepts a generic T which extends ActionTypes. If the payload (P) is undefined (value by default), it will return an object with only a type property; otherwise, it will return an object with type and payload properties.

Now you can use this utility type to define the different actions:


type Actions =
  | CreateAction<'toggleEditing'>
  | CreateAction<'enableEditing'>
  | CreateAction<'disableEditing', string>
  | CreateAction<'toggleModelA', { id: string }>
  | CreateAction<'toggleModelB'>
  | CreateAction<'updateData', Record<string, unknown>>;

Enter fullscreen mode Exit fullscreen mode

Creating Reducer Function

Time to really use ts-pattern by creating a reducer function


function reducer(state: State, action: Action): State {
  return match(action)
    .with({ type: 'toggleEditing' }, (event) => {
      // ...
    })
    .exhaustive();
}

Enter fullscreen mode Exit fullscreen mode

Here, we're using the match function from TS-Pattern to match the incoming action and handle each case. The .exhaustive() method ensures that every possible case is handled.

The usual way to do this is by using a switch statement like the following


function reducer(state: State, action: Action): State {
    switch(action.type) {
            case 'toggleEditing':
                    return state 
        }
        return state
}
Enter fullscreen mode Exit fullscreen mode

Can you spot the bug there?
It's easy to omit cases and Typescript doesn't give you any hint about it.

Also, there is another complexity. What if you need to "switch" on two different properties?

Let's say that form some actions you need perform different logic based on the payload, you may end with something like this


function reducer(state: State, action: Action): State {
    switch(action.type) {
            case 'toggleEditing':
                    return state 
        case 'toggleModalA':
            if(action.payload) {
                            // perform logic A 
                                return state 
                        }
                        if(action.payload === undefined) {
                            // perform logic B
                            return state 
                        }
        }
        return state
}
Enter fullscreen mode Exit fullscreen mode

And that can become really complex to read and maintain.

Let's go back to using pattern matching:


function reducer(state: State, action: Actions): State {
  return match({state,...action})
    .with({ type: "toggleEditing"}, toggleEditing)
    .with({ type: "enableEditing"}, (arg) => state)
    .with({ type: "disableEditing"}, () => state)
    .with({ type: "toggleModalA"}, toggleModalA)
    .with({ type: "toggleModalB"}, () => state)
    .with({ type: "updateData"}, () => state)
    .exhaustive()  
}


Enter fullscreen mode Exit fullscreen mode

Notice that each "code branch" execute a function, this function receives as arguments all the data that was used in the match method, in this case, each "callback" will receive an object like {state: State, type: ActionTypes, payload?: SOMETHING}

That means, that we can extract the logic into a separate function, pass the corresponding arguments. As result, the logic for each code branch will be a pure function that depends only on the arguments.

But, writing the types for each function arguments can be tedious, and we can do better by extracting the process into another utility type.

This utility type will generate teh correct arguments based on the action type.


type MatchEvent<T extends ActionTypes> = {
  state: State;
} & Extract<Action, { type: T }>;

Enter fullscreen mode Exit fullscreen mode

Now, you can define the action handling functions with the correct types:

const toggleEditing = (event: MatchEvent<'toggleEditing'>): State => {
  // ...
};

const toggleModelA = (event: MatchEvent<'toggleModelA'>): State => {
  // ...
};

Enter fullscreen mode Exit fullscreen mode

Using Reducer in a React Component

Finally, use the useReducer hook in a React component:


const App = () => {
  const [state, dispatch] = React.useReducer(reducer, initialState);

  // Use `dispatch` to update the state based on the action types
  dispatch({ type: 'toggleEditing' });

  // ...
};

Enter fullscreen mode Exit fullscreen mode

And that's it! You have now successfully implemented pattern matching with TS-Pattern in a React application. This technique allows for cleaner, more elegant code that is easier to read, maintain, and test. Enjoy exploring more possibilities with TS-Pattern and let us know what you think in the comments!

If you have any questions or need help, you can find me on Twitter or GitHub.

Footer Social Card.jpg
✉️ Únete a Micro-bytes 🐦 Sígueme en Twitter ❤️ Apoya mi trabajo

Top comments (0)