Rust pattern matching in Typescript
Tags: typescript, showdev, rust, productivity
Published: 2022-12-21 16:03:44

I get feature envy watching Rust tutorials. Seeing it's performant memory safe features wishing we had something similar in Typescript land.

Of all the features Rust has to offer the one most intriguing for me has to be it's pattern matching capabilities.

Pattern matching in Rust looks like this.

``` rust
let x = 1;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    _ => println!("anything"),
}
```

Given x it will match the value and log accordingly. If nothing matches it falls back to log "anything".

To do the same thing in Typescript would require nested ternary conditions, chaining ifs, or ugly switch statements.

``` typescript
const x = 1 as number;

// Ternary
x === 1
  ? console.log('one')
  : x === 2
  ? console.log('two')
  : x === 3
  ? console.log('three')
  : console.log('anything');

// If
if (x === 1) {
  console.log('one');
} else if (x === 2) {
  console.log('two');
} else if (x === 3) {
  console.log('three');
} else {
  console.log('anything');
}

// Switch
switch (x) {
  case 1: {
    console.log('one');
    break;
  }
  case 2: {
    console.log('two');
    break;
  }
  case 3: {
    console.log('three');
    break;
  }
  default:
    console.log('anything');
    break;
}
```

Things get out of hand very quickly when dealing with complex nested objects and arrays. Luckily we have libraries like zod and io-ts that make validating complex data incredibly simple. Problem is pattern matching complex unions still poses an issue.

Here's my attempt to solve this.

### zod-matcher

I recently released a library called zod-matcher taking inspiration from Rust's match syntax and building upon zod's  data validation to make it simple to pattern match any data.

``` typescript
declare const x : number

match(x)
    .case(z.literal(1), () => console.log('one'))
    .case(z.literal(2), () => console.log('two'))
    .case(z.literal(3), () => console.log('three'))
    .default(() => console.log('anything'))
    .parse()
```

Built entirely on Typescript it's as type strict as I can make it. Some cool features:

### Type safety
Unhandled cases won't allow you to parse. You must handle every case or fallback to a default.

``` typescript
const x = 'A' as 'A' | 'B'

// Error "Unhandled cases"
match(x)
    .case(z.literal('A'), console.log)
    .parse()

// Resolve by adding case
match(x)
    .case(z.literal('A'), console.log)
    .case(z.literal('B'), console.log)
    .parse()

// Or by adding default
match(x)
    .case(z.literal('A'), console.log)
    .default(console.log)
    .parse()
```

### Type narrowing
The .default() method passes the input value as an argument typed excluding any previously handled cases.

``` typescript
const x = 'A' as 'A' | 'B' | 'C';

match(x)
  .case(z.literal('A'), console.log)
  .case(z.literal('B'), console.log)
  .default(x => x) // <== Type of x is "C"
  .parse();
```
### Union return
The return type is a union of all the return types of each case.

``` typescript
const x = 'A' as 'A' | 'B' | 'C';

// Type of result is "A1" | "B2" | "C3"
const result = match(x)
  .case(z.literal('A'), x => `${x}1` as const)
  .case(z.literal('B'), x => `${x}2` as const)
  .case(z.literal('C'), x => `${x}3` as const)
  .parse();
```

### Safe parsing
You can throw on failed matches using .parse() or return a result union with .safeParse().

``` typescript
// Throws error if no match
const result = match(x)
    .case(z.string(), console.log)
    .parse()

// Returns result of union type
// | { success: true, data: x }
// | { success: false, error: MatcherError }
const result = match(x)
    .case(z.string(), console.log)
    .safeParse()
```

I like this syntax better than a ternary, if, or switch statement not to mention the benefits of type safety.

### Try it out!

```bash
yarn add zod-matcher
```

```bash
npm install zod-matcher
```