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"),
}
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;
}
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()
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()
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();
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();
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()
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
npm install zod-matcher
Top comments (0)