DEV Community

Evan Kanes
Evan Kanes

Posted on • Edited on

Rust pattern matching in Typescript

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.

let x = 1;

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

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.

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

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()
Enter fullscreen mode Exit fullscreen mode

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.

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()
Enter fullscreen mode Exit fullscreen mode

Type narrowing

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

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();
Enter fullscreen mode Exit fullscreen mode

Union return

The return type is a union of all the return types of each case.

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();
Enter fullscreen mode Exit fullscreen mode

Safe parsing

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

// 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()
Enter fullscreen mode Exit fullscreen mode

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

Try it out!

yarn add zod-matcher
Enter fullscreen mode Exit fullscreen mode
npm install zod-matcher
Enter fullscreen mode Exit fullscreen mode

Top comments (0)