Code Style
Pattern Matching

Pattern Matching

Pattern 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 (opens in a new tab) 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 effect/Match module provides a reliable, type-safe pattern matching implementation that is available for immediate use.

Defining a Matcher


Creating a Matcher involves using the type constructor function with a specified type. This sets the foundation for pattern matching against that particular type. Once the Matcher is established, developers can employ various combinators like when, not, and tag to define patterns that the Matcher will check against.

Here's a practical example:

import { Match } from "effect"
// $ExpectType (u: { a: number; } | { b: string; }) => string | number
const match = Match.type<{ a: number } | { b: string }>().pipe(
  Match.when({ a: Match.number }, (_) => _.a),
  Match.when({ b: Match.string }, (_) => _.b),
console.log(match({ a: 0 })) // Output: 0
console.log(match({ b: "hello" })) // Output: "hello"

Let's dissect what's happening:

  • Match.type<{ a: number } | { b: string }>(): This creates a Matcher for objects that are either of type { a: number } or { b: string }.
  • Match.when({ a: Match.number }, (_) => _.a): This sets up a condition to match an object with a property a containing a number. If matched, it returns the value of property a.
  • Match.when({ b: Match.string }, (_) => _.b): This condition matches an object with a property b containing a string. If found, it returns the value of property b.
  • Match.exhaustive: This function ensures that all possible cases are considered and matched, making sure that no other unaccounted cases exist. It helps to prevent overlooking any potential scenario.

Finally, the match function is applied to test two different objects, { a: 0 } and { b: "hello" }. As per the defined conditions within the Matcher, it correctly matches the objects and provides the expected output based on the defined conditions.


In addition to defining a Matcher based on a specific type, developers can also create a Matcher directly from a value utilizing the value constructor function. This method allows matching patterns against the provided value.

Let's take a look at an example to better understand this process:

import { Match } from "effect"
// $ExpectType string
const result = Match.value({ name: "John", age: 30 }).pipe(
    { name: "John" },
    (user) => `${} is ${user.age} years old`
  Match.orElse(() => "Oh, not John")
console.log(result) // Output: "John is 30 years old"

Here's a breakdown of what's happening:

  • Match.value({ name: "John", age: 30 }): This initializes a Matcher using the provided value { name: "John", age: 30 }.
  • Match.when({ name: "John" }, (user) => ...: It establishes a condition to match the object having the property name set to "John". If the condition is met, it constructs a string indicating the name and age of the user.
  • Match.orElse(() => "Oh, not John"): In the absence of a match with the name "John," this provides a default output.



Predicates allow the testing of values against specific conditions. It helps in creating rules or conditions for the data being evaluated.

import { Match } from "effect"
const match = Match.type<{ age: number }>().pipe(
  Match.when({ age: (age) => age >= 5 }, (user) => `Age: ${user.age}`),
  Match.orElse((user) => `${user.age} is too young`)
console.log(match({ age: 5 })) // Output: "Age: 5"
console.log(match({ age: 4 })) // Output: "4 is too young"


not allows for excluding a specific value while matching other conditions.

import { Match } from "effect"
const match = Match.type<string | number>().pipe(
  Match.not("hi", (_) => "a"),
  Match.orElse(() => "b")
console.log(match("hello")) // Output: "a"
console.log(match("hi")) // Output: "b"


The tag function enables pattern matching against the tag within a Discriminated Union (opens in a new tab).

import { Match, Either } from "effect"
const match = Match.type<Either.Either<string, number>>().pipe(
  Match.tag("Right", (_) => _.right),
  Match.tag("Left", (_) => _.left),
console.log(match(Either.right(123))) // Output: 123
console.log(match(Either.left("Oh no!"))) // Output: "Oh no!"

Note that it only works with the convention within the Effect ecosystem of naming the tag field with "_tag".

Transforming a Matcher


The exhaustive transformation serves as an endpoint within the matching process, ensuring all potential matches have been considered. It results in returning the match (for Match.value) or the evaluation function (for Match.type).

import { Match, Either } from "effect"
const result = Match.value(Either.right(0)).pipe(
  Match.when({ _tag: "Right" }, (_) => _.right),
  // @ts-expect-error
  Match.exhaustive // TypeError! Type 'Left<never, number>' is not assignable to type 'never'


The orElse transformation signifies the conclusion of the matching process, offering a fallback value when no specific patterns match. It returns the match (for Match.value) or the evaluation function (for Match.type).

import { Match } from "effect"
const match = Match.type<string | number>().pipe(
  Match.when("hi", (_) => "hello"),
  Match.orElse(() => "I literally do not understand")
console.log(match("hi")) // Output: "hello"
console.log(match("hello")) // Output: "I literally do not understand"


The option transformation returns the result encapsulated within an Option. When the match succeeds, it represents the result as Some, and when there's no match, it signifies the absence of a value with None.

import { Match, Either } from "effect"
// $ExpectType Option<number>
const result = Match.value(Either.right(0)).pipe(
  Match.when({ _tag: "Right" }, (_) => _.right),
console.log(result) // Output: { _id: 'Option', _tag: 'Some', value: 0 }


The either transformation might match a value, returning an Either following the format Either<NoMatchResult, MatchResult>.

import { Match } from "effect"
// $ExpectType (input: string) => Either<string, number>
const match = Match.type<string>().pipe(
  Match.when("hi", (_) => _.length),
console.log(match("hi")) // Output: { _id: 'Either', _tag: 'Right', right: 2 }
console.log(match("shigidigi")) // Output: { _id: 'Either', _tag: 'Left', left: 'shigidigi' }