ts-pattern-match
v0.2.0
Published
Pattern Matching for TypeScript.
Downloads
5
Readme
Pattern Matching for TypeScript
Extracted from the effect library.
Installation
npm install ts-pattern-matchPattern matching is a method that allows developers to handle intricate conditions within a single, concise expression. It simplifies code, making it more concise and easier to understand. Additionally, it includes a process called exhaustiveness checking, which helps to ensure that no possible case has been overlooked.
Originating from functional programming languages, pattern matching stands as a powerful technique for code branching. It often offers a more potent and less verbose solution compared to imperative alternatives such as if/else or switch statements, particularly when dealing with complex conditions.
Although not yet a native feature in JavaScript, there's an ongoing tc39 proposal in its early stages to introduce pattern matching to JavaScript. However, this proposal is at stage 1 and might take several years to be implemented. Nonetheless, developers can implement pattern matching in their codebase. The ts-pattern-match module provides a reliable, type-safe pattern matching implementation that is available for immediate use.
Example (Handling Different Data Types with Pattern Matching)
import { Match } from "ts-pattern-match";
// Simulated dynamic input that can be a string or a number
const input: string | number = "some input";
// ┌─── string
// ▼
const result = Match.value(input).pipe(
// Match if the value is a number
Match.when(Match.number, (n) => `number: ${n}`),
// Match if the value is a string
Match.when(Match.string, (s) => `string: ${s}`),
// Ensure all possible cases are covered
Match.exhaustive,
);
console.log(result);
// Output: "string: some input"How Pattern Matching Works
Pattern matching follows a structured process:
Creating a matcher. Define a
Matcherthat operates on either a specific type or value.Defining patterns. Use combinators such as
Match.when,Match.not, andMatch.tagto specify matching conditions.Completing the match. Apply a finalizer such as
Match.exhaustive,Match.orElse, orMatch.optionto determine how unmatched cases should be handled.
Creating a matcher
You can create a Matcher using either:
Match.type<T>(): Matches against a specific type.Match.value(value): Matches against a specific value.
Matching by Type
The Match.type constructor defines a Matcher that operates on a specific type. Once created, you can use patterns like Match.when to define conditions for handling different cases.
Example (Matching Numbers and Strings)
import { Match } from "ts-pattern-match";
// Create a matcher for values that are either strings or numbers
//
// ┌─── (u: string | number) => string
// ▼
const match = Match.type<string | number>().pipe(
// Match when the value is a number
Match.when(Match.number, (n) => `number: ${n}`),
// Match when the value is a string
Match.when(Match.string, (s) => `string: ${s}`),
// Ensure all possible cases are handled
Match.exhaustive,
);
console.log(match(0));
// Output: "number: 0"
console.log(match("hello"));
// Output: "string: hello"Matching by Value
Instead of creating a matcher for a type, you can define one directly from a specific value using Match.value.
Example (Matching an Object by Property)
import { Match } from "ts-pattern-match";
const input = { name: "John", age: 30 };
// Create a matcher for the specific object
const result = Match.value(input).pipe(
// Match when the 'name' property is "John"
Match.when(
{ name: "John" },
(user) => `${user.name} is ${user.age} years old`,
),
// Provide a fallback if no match is found
Match.orElse(() => "Oh, not John"),
);
console.log(result);
// Output: "John is 30 years old"Enforcing a Return Type
You can use Match.withReturnType<T>() to ensure that all branches return a specific type.
Example (Validating Return Type Consistency)
This example enforces that every matching branch returns a string.
import { Match } from "ts-pattern-match";
const match = Match.type<{ a: number } | { b: string }>().pipe(
// Ensure all branches return a string
Match.withReturnType<string>(),
// ❌ Type error: returns a number
// @errors: 2322
Match.when({ a: Match.number }, (_) => _.a),
// ✅ Correct: returns a string
Match.when({ b: Match.string }, (_) => _.b),
Match.exhaustive,
);The
Match.withReturnType<T>()call must be the first instruction in the pipeline. If placed later, TypeScript will not properly enforce return type consistency.
Defining patterns
when
The Match.when function allows you to define conditions for matching values. It supports both direct value comparisons and predicate functions.
Example (Matching with Values and Predicates)
import { Match } from "ts-pattern-match";
// Create a matcher for objects with an "age" property
const match = Match.type<{ age: number }>().pipe(
// Match when age is greater than 18
Match.when({ age: (age) => age > 18 }, (user) => `Age: ${user.age}`),
// Match when age is exactly 18
Match.when({ age: 18 }, () => "You can vote"),
// Fallback case for all other ages
Match.orElse((user) => `${user.age} is too young`),
);
console.log(match({ age: 20 }));
// Output: "Age: 20"
console.log(match({ age: 18 }));
// Output: "You can vote"
console.log(match({ age: 4 }));
// Output: "4 is too young"not
The Match.not function allows you to exclude specific values while matching all others.
Example (Ignoring a Specific Value)
import { Match } from "ts-pattern-match";
// Create a matcher for string or number values
const match = Match.type<string | number>().pipe(
// Match any value except "hi", returning "ok"
Match.not("hi", () => "ok"),
// Fallback case for when the value is "hi"
Match.orElse(() => "fallback"),
);
console.log(match("hello"));
// Output: "ok"
console.log(match("hi"));
// Output: "fallback"tag
The Match.tag function allows pattern matching based on the _tag field in a Discriminated Union. You can specify multiple tags to match within a single pattern.
Example (Matching a Discriminated Union by Tag)
import { Match } from "ts-pattern-match";
type Event =
| { readonly _tag: "fetch" }
| { readonly _tag: "success"; readonly data: string }
| { readonly _tag: "error"; readonly error: Error }
| { readonly _tag: "cancel" };
// Create a Matcher for Either<number, string>
const match = Match.type<Event>().pipe(
// Match either "fetch" or "success"
Match.tag("fetch", "success", () => `Ok!`),
// Match "error" and extract the error message
Match.tag("error", (event) => `Error: ${event.error.message}`),
// Match "cancel"
Match.tag("cancel", () => "Cancelled"),
Match.exhaustive,
);
console.log(match({ _tag: "success", data: "Hello" }));
// Output: "Ok!"
console.log(match({ _tag: "error", error: new Error("Oops!") }));
// Output: "Error: Oops!"Built-in Predicates
The Match module provides built-in predicates for common types, such as Match.number, Match.string, and Match.boolean. These predicates simplify the process of matching against primitive types.
Example (Using Built-in Predicates for Property Keys)
import { Match } from "ts-pattern-match";
const matchPropertyKey = Match.type<PropertyKey>().pipe(
// Match when the value is a number
Match.when(Match.number, (n) => `Key is a number: ${n}`),
// Match when the value is a string
Match.when(Match.string, (s) => `Key is a string: ${s}`),
// Match when the value is a symbol
Match.when(Match.symbol, (s) => `Key is a symbol: ${String(s)}`),
// Ensure all possible cases are handled
Match.exhaustive,
);
console.log(matchPropertyKey(42));
// Output: "Key is a number: 42"
console.log(matchPropertyKey("username"));
// Output: "Key is a string: username"
console.log(matchPropertyKey(Symbol("id")));
// Output: "Key is a symbol: Symbol(id)"| Predicate | Description |
| ------------------------- | ----------------------------------------------------------------------------- |
| Match.string | Matches values of type string. |
| Match.nonEmptyString | Matches non-empty strings. |
| Match.number | Matches values of type number. |
| Match.boolean | Matches values of type boolean. |
| Match.bigint | Matches values of type bigint. |
| Match.symbol | Matches values of type symbol. |
| Match.date | Matches values that are instances of Date. |
| Match.record | Matches objects where keys are string or symbol and values are unknown. |
| Match.null | Matches the value null. |
| Match.undefined | Matches the value undefined. |
| Match.defined | Matches any defined (non-null and non-undefined) value. |
| Match.any | Matches any value without restrictions. |
| Match.is(...values) | Matches a specific set of literal values (e.g., Match.is("a", 42, true)). |
| Match.instanceOf(Class) | Matches instances of a given class. |
Completing the match
exhaustive
The Match.exhaustive method finalizes the pattern matching process by ensuring that all possible cases are accounted for. If any case is missing, TypeScript will produce a type error. This is particularly useful when working with unions, as it helps prevent unintended gaps in pattern matching.
Example (Ensuring All Cases Are Covered)
import { Match } from "ts-pattern-match";
// Create a matcher for string or number values
const match = Match.type<string | number>().pipe(
// Match when the value is a number
Match.when(Match.number, (n) => `number: ${n}`),
// Mark the match as exhaustive, ensuring all cases are handled
// TypeScript will throw an error if any case is missing
// @errors: 2345
Match.exhaustive,
);orElse
The Match.orElse method defines a fallback value to return when no other patterns match. This ensures that the matcher always produces a valid result.
Example (Providing a Default Value When No Patterns Match)
import { Match } from "ts-pattern-match";
// Create a matcher for string or number values
const match = Match.type<string | number>().pipe(
// Match when the value is "a"
Match.when("a", () => "ok"),
// Fallback when no patterns match
Match.orElse(() => "fallback"),
);
console.log(match("a"));
// Output: "ok"
console.log(match("b"));
// Output: "fallback"option
Match.option wraps the match result in an Option. If a match is found, it returns Some(value), otherwise, it returns None.
Example (Extracting a User Role with Option)
import { Match } from "ts-pattern-match";
type User = { readonly role: "admin" | "editor" | "viewer" };
// Create a matcher to extract user roles
const getRole = Match.type<User>().pipe(
Match.when({ role: "admin" }, () => "Has full access"),
Match.when({ role: "editor" }, () => "Can edit content"),
Match.option, // Wrap the result in an Option
);
console.log(getRole({ role: "admin" }));
// Output: { _id: 'Option', _tag: 'Some', value: 'Has full access' }
console.log(getRole({ role: "viewer" }));
// Output: { _id: 'Option', _tag: 'None' }either
The Match.either method wraps the result in an Either, providing a structured way to distinguish between matched and unmatched cases. If a match is found, it returns Right(value), otherwise, it returns Left(no match).
Example (Extracting a User Role with Either)
import { Match } from "ts-pattern-match";
type User = { readonly role: "admin" | "editor" | "viewer" };
// Create a matcher to extract user roles
const getRole = Match.type<User>().pipe(
Match.when({ role: "admin" }, () => "Has full access"),
Match.when({ role: "editor" }, () => "Can edit content"),
Match.either, // Wrap the result in an Either
);
console.log(getRole({ role: "admin" }));
// Output: { _id: 'Either', _tag: 'Right', right: 'Has full access' }
console.log(getRole({ role: "viewer" }));
// Output: { _id: 'Either', _tag: 'Left', left: { role: 'viewer' } }