npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ts-pattern-binding

v0.3.0

Published

Bind a value in a ts-pattern pattern and compare later positions, inspired by Elixir's repeated variables.

Readme

ts-pattern-binding

Bind a value at one position in a ts-pattern pattern. Require the same value at later positions.

For example, an order and its payment must have the same customer ID:

import { P, match } from 'ts-pattern'
import { binding } from 'ts-pattern-binding'

const order = { customer: { id: 'customer-123' } }
const payment = { customer: { id: 'customer-123' } }

const result = match({ order, payment })
  .with(
    binding(P.string, ({ bind, ref }) => ({
      order: { customer: { id: bind } },
      payment: { customer: { id: ref } }
    })),
    () => true
  )
  .otherwise(() => false)

result is true. If the payment's customer ID is 'customer-456', the result is false.

bind requires order.customer.id to be a string and stores it. ref requires payment.customer.id to equal that stored string, using Object.is. Both fields belong to one pattern passed to .with. bind and ref are pattern values, so neither needs ().

The pattern argument is optional. binding(P.string, callback) narrows both positions to string, including refs in the handler. binding(callback) accepts any value and leaves unknown inputs as unknown.

Place bind before ref in the pattern. Create a fresh binding for each match operation. The state lifetime rules explain reuse and more complex patterns.

Install

npm install ts-pattern-binding ts-pattern

The package supports ts-pattern 5.9.x, ESM, and CommonJS. It includes TypeScript declarations for both module formats. ts-pattern is a peer dependency and stays outside the build.

Why this library exists

Elixir supports repeated variables within one pattern. Each occurrence must match the same value:

order = %{customer: %{id: "customer-123"}}
payment = %{customer: %{id: "customer-123"}}

case %{order: order, payment: payment} do
  %{order: %{customer: %{id: customer_id}}, payment: %{customer: %{id: customer_id}}} -> true
  _ -> false
end

Elixir also supports the pin operator, ^, for a value that exists before the pattern. This library implements the repeated-variable use case with explicit bind and ref positions. The Elixir pattern documentation describes both forms.

Why ts-pattern does not provide this directly

ts-pattern accepts patterns made from JavaScript values and matcher objects. JavaScript evaluates a variable before ts-pattern receives the pattern. Repeated JavaScript references therefore represent existing values, rather than variables that ts-pattern can bind during a match.

P.select('customerId') passes a value to the handler. It does not impose an equality constraint on another position with the same selection name. P.when receives the value at its own position. It does not receive sibling values or a shared variable environment. These APIs appear in the ts-pattern documentation.

The library adds a shared state cell behind two P.when patterns. One pattern writes the value. The other pattern compares its value against that cell. The implementation uses the public API and does not import ts-pattern internals.

ts-pattern can already express the same condition with a guard on the whole input:

import { P, match } from 'ts-pattern'

const order = { customer: { id: 'customer-123' } }
const payment = { customer: { id: 'customer-123' } }

const result = match({ order, payment })
  .with(
    {
      order: { customer: { id: P.string } },
      payment: { customer: { id: P.string } }
    },
    ({
      order: {
        customer: { id: orderCustomerId }
      },
      payment: {
        customer: { id: paymentCustomerId }
      }
    }) => Object.is(orderCustomerId, paymentCustomerId),
    () => true
  )
  .otherwise(() => false)

binding places each equality constraint beside the field that it concerns. This form is useful for nested patterns and several independent constraints.

API

binding(pattern?)

Each call creates an independent binding. The returned instance contains stable, readonly bind and ref patterns. An optional ts-pattern pattern constrains both positions and supplies their inferred type. Without a pattern, bind accepts any value and ref only requires equality.

import { isMatching } from 'ts-pattern'
import { binding } from 'ts-pattern-binding'

const { bind, ref } = binding()

const allEqual = isMatching([bind, ref, ref], [42, 42, 42])

allEqual is true. If the last value is 43, the pattern fails. The first position stores 42. Both references must match that value. Each successful visit to bind replaces the stored value. A value that fails the supplied pattern leaves the previous binding unchanged. ref never changes the stored value.

binding(callback) and binding(pattern, callback)

Each call creates a Binding and calls the callback once. binding returns the pattern from the callback. The callback runs before ts-pattern evaluates that pattern. The returned pattern preserves narrowing, selections, and composition.

Without a supplied pattern, the binding accepts any value:

import { isMatching } from 'ts-pattern'
import { binding } from 'ts-pattern-binding'

const allEqual = isMatching(
  binding(({ bind, ref }) => [bind, ref, ref]),
  [42, 42, 42]
)

allEqual is true. binding(callback) leaves unknown inputs as unknown. binding(pattern, callback) applies the supplied pattern to both bind and ref. binding(undefined, callback) requires literal undefined values.

Nested callbacks create independent bindings:

import { P, isMatching } from 'ts-pattern'
import { binding } from 'ts-pattern-binding'

const pattern = binding(P.string, ({ bind: bindName, ref: sameName }) =>
  binding(P.number, ({ bind: bindId, ref: sameId }) => [
    bindName,
    bindId,
    sameName,
    sameId
  ])
)

const sameNameAndId = isMatching(pattern, ['customer', 42, 'customer', 42])

sameNameAndId is true. The name and ID have separate bindings and separate runtime patterns.

The callback receives the full Binding instance, including reset(). The returned pattern does not expose that instance. binding() and new Binding() remain available for direct access to the instance.

new Binding(pattern?)

The class constructor is equivalent to the factory without a callback. Instances support instanceof Binding. new Binding(P.string) infers string patterns. new Binding() leaves unknown inputs as unknown.

reset(): void

reset clears the stored value. Every reference then fails until the next bind. The existing pattern objects remain valid.

import { isMatching } from 'ts-pattern'
import { binding } from 'ts-pattern-binding'

const customerId = binding()
const { bind, ref } = customerId

isMatching(bind, 'customer-123')
const beforeReset = isMatching(ref, 'customer-123')
customerId.reset()
const afterReset = isMatching(ref, 'customer-123')

beforeReset is true. afterReset is false, even though the customer ID is unchanged. The state distinguishes an unbound value from a bound undefined. Before a reset, ref can match a bound undefined.

Equality

The library uses JavaScript's Object.is equality:

| Values | Result | | -------------------------------------------- | -------- | | Equal strings, booleans, numbers, or bigints | Match | | NaN and NaN | Match | | 0 and -0 | No match | | null and undefined | No match | | The same object, array, function, or symbol | Match | | Separate objects with identical contents | No match |

This choice preserves the original helper's behavior. Elixir compares terms by value, including maps and lists. JavaScript objects use reference identity here.

Selections

P.select passes the matching customer ID to the handler:

import { P, match } from 'ts-pattern'
import { binding } from 'ts-pattern-binding'

const order = { customer: { id: 'customer-123' } }
const payment = { customer: { id: 'customer-123' } }
const { bind, ref } = binding(P.string)

const result = match({ order, payment })
  .with(
    {
      order: { customer: { id: P.select('customerId', bind) } },
      payment: { customer: { id: ref } }
    },
    ({ customerId }) => customerId
  )
  .otherwise(() => 'Customer IDs differ')

result is 'customer-123'. P.select('customerId', bind) stores the value and selects it for the handler. ref requires the payment's customer ID to match that value.

Types and runtime guards

The supplied pattern determines the runtime constraint and the inferred types of both matchers. Strings, literals, unions, arrays, and object patterns retain the types that ts-pattern infers. ref checks the pattern again after equality succeeds. This checks the current shape of a retained object if its contents change.

For unknown inputs, pass the runtime pattern to binding:

import { P, match } from 'ts-pattern'
import { binding } from 'ts-pattern-binding'

const { bind, ref } = binding(P.string)
const input: unknown = {
  order: { customer: { id: 'customer-123' } },
  payment: { customer: { id: 'customer-123' } }
}

const result = match(input)
  .with(
    {
      order: { customer: { id: bind } },
      payment: { customer: { id: ref } }
    },
    ({
      payment: {
        customer: { id }
      }
    }) => id.toUpperCase()
  )
  .otherwise(() => 'Invalid or different customer IDs')

result is 'CUSTOMER-123'. Both customer IDs have type string in the handler. The handler uses the payment's ID, which matched ref. Equal numbers fail the supplied string pattern.

Without a pattern, both matchers leave unknown values as unknown. The existing binding<T>() and new Binding<T>() forms still record an intended input type without runtime validation. For example, binding<string>() accepts equal numbers and does not narrow unknown inputs to strings. A separate P.intersection(P.string, bind) guard does not change a reference's inferred type.

binding(undefined) requires a literal undefined value. Omitting the argument with binding() accepts any value. Selections inside a supplied pattern do not pass values to the handler. Use P.select around bind or ref to select a value.

An equality-dependent branch does not make a match exhaustive. It still needs a fallback or other branches that cover the remaining inputs. bind also requires a fallback because a supplied pattern can accept only part of its inferred type.

BindingPattern<T, Narrowed> and ReferencePattern<T, Narrowed> are exported for explicit annotations. Narrowed defaults to unknown.

Evaluation order and state lifetime

These patterns contain mutable state. Each reference must run after its bind. ts-pattern visits object keys in the pattern's enumeration order and tuple positions from left to right. Numeric object keys run in numeric order, even after a different insertion order. The ts-pattern matcher implementation determines this traversal.

For ordinary object keys, place the bind before the references:

import { P, isMatching } from 'ts-pattern'
import { binding } from 'ts-pattern-binding'

const order = { customer: { id: 'customer-123' } }
const payment = { customer: { id: 'customer-123' } }
const { bind, ref } = binding(P.string)

const result = isMatching(
  {
    order: { customer: { id: bind } },
    payment: { customer: { id: ref } }
  },
  { order, payment }
)

result is true. A fresh binding fails if the pattern visits payment.customer.id before order.customer.id. Input property order does not change pattern order.

Create a fresh binding inside each match operation. If a function performs the match, create the binding inside the function.

The callback forms create a binding when they construct the pattern. Reuse of the returned pattern reuses the same binding. The callback does not run again, and evaluation does not automatically reset the binding.

The matcher API does not notify these patterns when an enclosing branch fails or a match finishes. A failed property, reference, or guard does not undo a previous bind. State remains until another bind or reset. Each binding therefore needs its own lifetime.

  • Use separate bindings for separate .with branches and union alternatives.
  • Place one bind before its references on every evaluation path.
  • If a pattern repeats in P.array, make sure that each element visits its bind before its references.
  • Avoid bind or ref inside P.not.
  • Avoid bind inside an optional pattern that can skip it.
  • Avoid a shared binding across concurrent calls or nested matches.
  • If you retain a binding, call reset before a new independent operation.

Repeated bind positions overwrite the value. Use ref for each repeated-variable position after the first one. These constraints distinguish this helper from Elixir's transactional pattern matching.

Development

mise install
mise run install
mise run check

mise run check runs the compiler, type-aware linting, formatting, coverage, the build, and package checks. The tests cover equality, nested patterns, selections, state lifetime, evaluation order, and type inference. Property tests compare arbitrary values against Object.is. Coverage must reach 100% for statements, branches, functions, and lines.

The compiler configuration includes every strict check from chatbot-farmacias. It also enables verbatimModuleSyntax and isolatedDeclarations, and checks dependency declarations with skipLibCheck: false. Lint rules reject any, unsafe operations, type assertions, non-null assertions, ternaries, and try statements in TypeScript. better-typescript-lib replaces unsafe standard-library return types with unknown.

The package checks install the actual tarball in a temporary consumer. They run ESM and CommonJS smoke tests and compile consumers for both declaration formats. CI runs these checks on Node 22, 24, and 26.

Releases

Release configuration and first-publication instructions are in RELEASING.md.

License

MIT.