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

@anilkumarthakur/match

v0.2.0

Published

PHP-style match expressions for JavaScript/TypeScript

Readme

@anilkumarthakur/match

npm version license CI coverage

PHP-style match expressions for JavaScript/TypeScript with 100% type safety and comprehensive test coverage.

@anilkumarthakur/match brings the power and elegance of PHP's match expression to the JavaScript/TypeScript world. It provides a clean, type-safe alternative to complex switch statements and nested if-else logic.

Features

✨ Type-Safe: Full TypeScript support with generic types for subject and result
🎯 Readable: Clean, expressive syntax inspired by PHP match expressions
🚀 Fast: Eager, allocation-free matching — a single Object.is per case, no lookup table to build
🛡️ Guards: Predicate functions, not just literals — the headline extension over PHP's match
📦 Lightweight: Zero dependencies, ~1 KB gzipped (ES module)
🧪 Well-Tested: Comprehensive test suite at 100% code coverage
🔗 Chainable: Fluent API for method chaining
🌍 Cross-Platform: Works in Node.js and browsers (ESM + CJS + IIFE browser global)

Installation

npm

npm install @anilkumarthakur/match

yarn

yarn add @anilkumarthakur/match

bun

bun add @anilkumarthakur/match

CDN (browser global)

unpkg and jsDelivr resolve the bare package specifier to dist/index.global.js, an IIFE bundle that exposes everything on the JsMatch global. There is no UMD build — the bundle performs no AMD/CommonJS detection.

<script src="https://unpkg.com/@anilkumarthakur/match"></script>
<script>
  const { match } = JsMatch
  console.log(
    match(404)
      .on(404, () => 'Not Found')
      .otherwise(() => 'Unknown')
  ) // "Not Found"
</script>

Quick Start

import { match } from '@anilkumarthakur/match'

const result = match('success')
  .on('success', () => 'Operation successful!')
  .on('error', () => 'Something went wrong')
  .otherwise(() => 'Unknown status')

console.log(result) // "Operation successful!"

API Reference

match<TSubject, TResult>(subject: TSubject): Matcher

Creates a new match expression for the given subject value.

Parameters:

  • subject - The value to match against (any type)

Returns: A Matcher instance for method chaining

Example:

const matcher = match(statusCode)

on(pattern: TSubject | Predicate<TSubject>, handler: () => TResult): Matcher

Adds a case. pattern is either a literal value compared with Object.is(), or a predicate function (subject) => boolean (see Predicate Matching).

Parameters:

  • pattern - A literal value, or a predicate function receiving the subject
  • handler - Function returning the result if matched

Returns: The matcher instance (for chaining)

Example:

match(status)
  .on(200, () => 'Success')
  .on(404, () => 'Not Found')

onAny(values: readonly TSubject[], handler: () => TResult): Matcher

Adds multiple values that all map to the same handler (simulates PHP's comma-separated cases).

Parameters:

  • values - Array of values to match
  • handler - Function to execute if any value matches

Returns: The matcher instance (for chaining)

Example:

match(status)
  .onAny([200, 201, 202], () => 'Success')
  .onAny([400, 401, 403], () => 'Client Error')

otherwise(handler: () => TResult): TResult

Sets the default handler and executes the match. Returns immediately with the result.

Parameters:

  • handler - Function to execute if no cases match

Returns: The result from matched handler or default handler

Throws: UnhandledMatchError if no match found and no default provided

Example:

const result = match(value)
  .on('expected', () => 'matched')
  .otherwise(() => 'default')

default(handler: () => TResult): TResult

PHP-compatible alias for otherwise(). Identical behavior.

Example:

const result = match(value)
  .on('expected', () => 'matched')
  .default(() => 'default')

get(): TResult

Returns the matched result without a default handler. Throws if nothing matched.

Returns: The result from the matched handler

Throws: UnhandledMatchError if no match found

Example:

const result = match('test')
  .on('test', () => 'matched')
  .get() // Must have matched something

valueOf(): TResult

Deprecated alias for get(). Prefer get().

valueOf is JavaScript's own ToPrimitive hook, so the engine calls it implicitly whenever a matcher is coerced — string concatenation, ==, arithmetic. On an unmatched chain that means an UnhandledMatchError can surface from an expression that never mentions valueOf:

const matcher = match(1).on(2, () => 'two')
matcher + '' // throws UnhandledMatchError: Unhandled match value: 1

get() has no such coupling to the language.

run(): boolean

Executes the chain for side effects only and reports whether anything matched. Handlers have already run by this point (matching is eager), so this is just the terminal that gives you the boolean instead of a value.

Returns: true if a case matched, false otherwise

Example:

const handled = match(action)
  .on('save', () => saveData())
  .on('delete', () => deleteData())
  .run()

if (!handled) console.warn(`Unknown action: ${action}`)

isMatched: boolean

Read-only getter for the current match state. Unlike run() it does not terminate the chain, so you can inspect a matcher and keep adding cases.

Example:

const matcher = match('test').on('test', () => 'matched')
console.log(matcher.isMatched) // true

UnhandledMatchError

Custom error thrown when no case matches and no default handler is provided.

Properties:

  • name - "UnhandledMatchError"
  • message - Contains the unmatched value

Example:

try {
  match('foo')
    .on('bar', () => 'bar')
    .get()
} catch (error) {
  if (error instanceof UnhandledMatchError) {
    console.error('No match found for:', error.message)
  }
}

Usage Examples

Predicate Matching

Beyond PHP: any on() pattern may be a function, in which case it is called with the subject and matches when it returns truthy.

const grade = (score: number): string => {
  return match(score)
    .on(
      (n) => n >= 90,
      () => 'A'
    )
    .on(
      (n) => n >= 80,
      () => 'B'
    )
    .on(
      (n) => n >= 70,
      () => 'C'
    )
    .otherwise(() => 'F')
}

console.log(grade(95)) // "A"
console.log(grade(85)) // "B"
console.log(grade(45)) // "F"

Literals and predicates mix freely in one chain — first match wins either way:

const describe = (score: number | string): string => {
  return match(score)
    .on('N/A', () => 'Not available')
    .on(
      (v) => typeof v === 'number' && v > 80,
      () => 'High'
    )
    .otherwise(() => 'Low')
}

console.log(describe('N/A')) // "Not available"
console.log(describe(90)) // "High"
console.log(describe(60)) // "Low"

A function subject disables predicate matching. When the subject is itself a function, on() compares patterns by reference instead of calling them — otherwise you could never match a function value literally. The predicate is silently never invoked and the case just falls through, with no error. See Supported Types.

Basic String Matching

import { match } from '@anilkumarthakur/match'

const getRole = (role: string) => {
  return match(role)
    .on('admin', () => 'Full access')
    .on('user', () => 'Limited access')
    .on('guest', () => 'Read-only access')
    .otherwise(() => 'Unknown role')
}

console.log(getRole('admin')) // "Full access"

Number Matching (HTTP Status Codes)

const handleResponse = (statusCode: number) => {
  return match(statusCode)
    .on(200, () => 'OK')
    .onAny([201, 202, 204], () => 'Created/Accepted')
    .on(400, () => 'Bad Request')
    .on(401, () => 'Unauthorized')
    .on(404, () => 'Not Found')
    .on(500, () => 'Server Error')
    .otherwise(() => 'Unknown Status')
}

console.log(handleResponse(200)) // "OK"
console.log(handleResponse(201)) // "Created/Accepted"
console.log(handleResponse(999)) // "Unknown Status"

Complex Notifications

const showNotification = (type: string, message: string) => {
  const styling = match(type)
    .on('success', () => ({ color: 'green', icon: '✓' }))
    .on('error', () => ({ color: 'red', icon: '✗' }))
    .on('warning', () => ({ color: 'orange', icon: '⚠' }))
    .on('info', () => ({ color: 'blue', icon: 'ℹ' }))
    .otherwise(() => ({ color: 'gray', icon: '•' }))

  return `[${styling.icon}] ${message}`
}

console.log(showNotification('success', 'Saved!')) // "[✓] Saved!"
console.log(showNotification('error', 'Failed!')) // "[✗] Failed!"

Nested Match Expressions

const getUserStatus = (userId: string, status: string) => {
  return match(userId)
    .on('admin', () => {
      return match(status)
        .on('active', () => 'Admin is active')
        .on('inactive', () => 'Admin is inactive')
        .otherwise(() => 'Admin status unknown')
    })
    .on('user', () => {
      return match(status)
        .on('active', () => 'User is active')
        .otherwise(() => 'User is inactive')
    })
    .otherwise(() => 'User not found')
}

console.log(getUserStatus('admin', 'active')) // "Admin is active"
console.log(getUserStatus('user', 'active')) // "User is active"
console.log(getUserStatus('guest', 'active')) // "User not found"

Type-Safe Unions

type LogLevel = 'debug' | 'info' | 'warn' | 'error'

const getLogColor = (level: LogLevel): string => {
  return match(level)
    .on('debug', () => 'gray')
    .on('info', () => 'blue')
    .on('warn', () => 'yellow')
    .on('error', () => 'red')
    .otherwise(() => 'white')
}

console.log(getLogColor('info')) // "blue"

Conditional Logic with match(true)

const getUserMessage = (age: number, isPremium: boolean) => {
  return match(true)
    .on(age < 13, () => 'Not eligible')
    .on(age >= 13 && age < 18, () => 'Teen user')
    .on(age >= 18 && !isPremium, () => 'Free user')
    .on(age >= 18 && isPremium, () => 'Premium user')
    .otherwise(() => 'Unknown')
}

console.log(getUserMessage(25, true)) // "Premium user"
console.log(getUserMessage(16, false)) // "Teen user"

Days in Month (Real-World Example)

const daysInMonth = (month: string, year: number): number => {
  const isLeap = (y: number) => y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0)

  return match(month.toLowerCase().slice(0, 3))
    .on('jan', () => 31)
    .on('feb', () => (isLeap(year) ? 29 : 28))
    .on('mar', () => 31)
    .on('apr', () => 30)
    .on('may', () => 31)
    .on('jun', () => 30)
    .on('jul', () => 31)
    .on('aug', () => 31)
    .on('sep', () => 30)
    .on('oct', () => 31)
    .on('nov', () => 30)
    .on('dec', () => 31)
    .otherwise(() => {
      throw new Error('Invalid month')
    })
}

console.log(daysInMonth('February', 2024)) // 29 (leap year)
console.log(daysInMonth('February', 2025)) // 28

Comparison with PHP match()

PHP

$result = match($status) {
    'success', 'ok' => 'All good',
    'error', 'fail' => 'Something went wrong',
    default => 'Unknown'
};

JavaScript (this library)

const result = match(status)
  .onAny(['success', 'ok'], () => 'All good')
  .onAny(['error', 'fail'], () => 'Something went wrong')
  .otherwise(() => 'Unknown')

Supported Types

The library supports matching on any JavaScript type. Literal comparison uses Object.is(), not ===:

  • ✅ Strings
  • ✅ Numbers (including Infinity, -Infinity, NaN)
  • ✅ Booleans
  • ✅ null / undefined
  • ✅ Symbols
  • ✅ BigInt
  • ✅ Objects (by reference)
  • ✅ Arrays (by reference)
  • ✅ Functions (by reference)
  • ✅ Enums
  • ✅ Class instances (by reference)

Object.is(), not ===

The two operators diverge on exactly two values, and the difference is deliberate:

// NaN matches NaN — `NaN === NaN` is false, `Object.is(NaN, NaN)` is true
match(NaN)
  .on(NaN, () => 'matched NaN')
  .otherwise(() => 'no match') // → "matched NaN"

// +0 and -0 are distinct — `+0 === -0` is true, `Object.is(+0, -0)` is false
match(+0)
  .on(-0, () => 'negative zero')
  .on(+0, () => 'positive zero')
  .otherwise(() => 'default') // → "positive zero"

onAny() uses Object.is() per element, so the same rules apply there.

Function subjects

Because functions are matched by reference, a function-valued subject turns off predicate matching for the whole chain — the pattern is compared to the subject rather than called:

const fn = () => 'subject'

match(fn)
  .on(
    (v) => true,
    () => 'predicate ran'
  ) // never invoked
  .otherwise(() => 'fell through') // → "fell through"

match(fn)
  .on(fn, () => 'matched by reference')
  .otherwise(() => 'no match') // → "matched by reference"

There is no error or warning — the case simply does not fire. If you need guards over a function value, match on something else (match(true) with boolean conditions, or a derived key).

Type Safety

Full TypeScript support with automatic type inference:

// Explicit types
const result = match<string, number>('test')
  .on('test', () => 123)
  .otherwise(() => 456)

// Inferred types
const result2 = match('test')
  .on('test', () => 'result') // Inferred as string result
  .otherwise(() => 'default')

// Union types
type Status = 'success' | 'pending' | 'error'
const result3 = match<Status, string>('success')
  .on('success', () => 'Done')
  .on('pending', () => 'In progress')
  .on('error', () => 'Failed')
  .otherwise(() => 'Unknown')

Performance

  • ⚡ Sequential Object.is() scan — O(N) in the number of cases, with no Map and no lookup table to allocate. Eager chaining is what makes that inherent: a handler must be able to run the moment its .on() is evaluated, so there is nothing to index ahead of time. For the case counts a match expression realistically has, a few identity comparisons beat building a hash map.
  • 💾 Only the first matching handler executes; later .on() calls short-circuit to no-ops
  • 📦 Roughly 3 kB raw / 1 kB gzipped per format (ESM, CJS, IIFE)

Testing

The library ships a comprehensive test suite at 100% code coverage:

# Run all tests
npm test

# Run with coverage
npm run test:coverage

# Watch mode
npm test -- --watch

Test categories:

  • Basic functionality
  • Type matching (strings, numbers, booleans, objects, arrays, etc.)
  • All API methods (on, onAny, otherwise, default, get/valueOf, run, isMatched)
  • Predicate matching and function subjects
  • Error handling
  • Type safety
  • Real-world examples
  • Edge cases and performance

Browser Support

Works in all modern browsers. The published engines.node range is >=22, and the build targets ES2022. The Node floor follows the LTS schedule — it is the oldest LTS line still in maintenance and rises as older lines reach end-of-life — so the supported runtimes are:

  • Chrome/Edge (latest)
  • Firefox (latest)
  • Safari (latest)
  • Node.js 22+ (active LTS lines)

Contributing

Contributions are welcome! Please feel free to submit issues and pull requests.

Development

This is a pnpm workspace; run these from the repository root.

# Install dependencies
pnpm install

# Run tests
pnpm run test

# Build
pnpm run build

# Lint and format
pnpm run lint
pnpm run format

# Typecheck + lint + format:check + test, in one shot
pnpm run verify

License

MIT - See LICENSE for details

Author

Anil Kumar Thakur

Related

Changelog

See CHANGELOG.md for release history and updates.