# tiny-pattern-ts Pattern matching for TypeScript/ESM environments (F#-style, not regex). ## Synopsis ```sh npm install tiny-pattern-ts ``` ```ts import { match, P } from "tiny-pattern-ts"; const reply = (answer: "yes" | "no") => match(answer) .with(P.literal("yes"), (): "agreed" => "agreed") .with(P.literal("no"), (): "declined" => "declined") .exhaustive(); reply("yes"); // "agreed" ``` ### Requirements - **Node.js >= 26** (`engines` field; pinned via `.node-version`). - **TypeScript >= 5.0** to consume the published declarations. The emitted `.d.ts` use `const` type parameters (TS 5.0) and keep their relative `.ts` specifiers; both resolve on TS >= 5.0 in `node10` / `node16` / `nodenext` / `bundler`. - The package is **ESM-only** (no CommonJS shim). ## Description `tiny-pattern-ts` gives TypeScript the shape of F#-style pattern matching: a value flows through a chain of patterns, the first one that matches runs its handler, and the handler receives the value narrowed to that pattern's type. The "patterns" are ordinary objects whose `matches` method is a TypeScript type guard, so narrowing composes the way any other guard does. It is deliberately not a regex engine and not a macro. There is no transpiler and no DSL to learn: `match(value)` returns a builder, `.with(pattern, handler)` adds a case, and the chain ends in either `.exhaustive()` or `.otherwise(...)`. The type-level contract is the feature — see [development/library.md](./development/library.md) for the design decisions and the known limitations. ## Examples ### Literal matching and `exhaustive()` `.exhaustive()` returns the union of the handler return types and throws if no case matched. Annotate handler returns when you want literal types rather than `string`: ```ts type Answer = "yes" | "no"; const reply = (answer: Answer): "agreed" | "declined" => match(answer) .with(P.literal("yes"), (): "agreed" => "agreed") .with(P.literal("no"), (): "declined" => "declined") .exhaustive(); reply("yes"); // "agreed" ``` `exhaustive()` checks at runtime, not at compile time — TypeScript does not force every union member to have a case (see [development/library.md](./development/library.md#exhaustive-is-a-runtime-check)). Use `.otherwise(...)` when a fallback is wanted: ```ts const label = (answer: Answer): string => match(answer) .with(P.literal("yes"), () => "agreed") .otherwise(() => "not agreed"); ``` ### Matching by `typeof` `P.type(name)` pairs an explicit type `T` with the runtime `typeof` name it should test for: ```ts const describe = (value: unknown): string => match(value) .with(P.type("string"), (s) => `string of length ${s.length}`) .with(P.type("number"), (n) => `number ${n.toFixed(2)}`) .otherwise(() => "something else"); ``` The supported names are `string`, `number`, `boolean`, `bigint`, `symbol`, `undefined`, `object`, and `function`. `"object"` matches non-null objects and functions; `"undefined"` compares against `undefined` directly. ### Structural matching and discriminated unions `P.shape(shape, refine?)` checks that every key in `shape` exists on the value. A value that is itself a matcher is applied, otherwise it is compared with strict equality. To narrow to a concrete type, pass a `refine` type guard: ```ts interface Circle { readonly kind: "circle"; readonly radius: number; } interface Square { readonly kind: "square"; readonly side: number; } type Shape = Circle | Square; const area = (shape: Shape): number => match(shape) .with( P.shape({ kind: "circle" }, (v): v is Circle => "radius" in v), (c) => Math.PI * c.radius ** 2, ) .with( P.shape({ kind: "square" }, (v): v is Square => "side" in v), (s) => s.side ** 2, ) .exhaustive(); ``` Without `refine`, `P.shape` returns a matcher for the shape's own type, not the narrowed one. Nested matchers can be used in the shape object, for example `P.shape({ name: P.type("string") })`. ### Custom guards with `when` `P.when` takes a type guard and infers the narrowed type from it: ```ts const toNumber = (value: unknown): number => match(value) .with( P.when((v): v is string => typeof v === "string"), (s) => Number.parseInt(s, 10), ) .otherwise(() => 0); ``` ### Widening with `any` `P.any(predicate)` takes a plain boolean predicate and a declared type `T`, for cases where the predicate cannot be written as a type guard: ```ts const firstNumber = (items: readonly unknown[]): number | undefined => match(items) .with( P.any( (v) => Array.isArray(v) && v.every((item) => typeof item === "number"), ), (xs) => xs[0], ) .otherwise(() => undefined); ``` ## API ### `match(value)` ```ts const match: (value: T) => MatchBuilder; ``` Starts a matching chain for `value`. The builder is immutable: every `.with` returns a new builder, so a partially built chain can be reused. #### `.with(pattern, handler)` ```ts with(pattern: Matcher, handler: (value: U) => V): MatchBuilder; ``` Adds a case. `handler` receives the value narrowed to `U`, and its return type `V` is added to the builder's result union `R`. A pattern whose narrowed type is not assignable to the matched value's type is a compile error. #### `.exhaustive()` ```ts exhaustive(): R; ``` Returns the result of the first matching case. Throws `tiny-pattern-ts: match.exhaustive() called with no matching case` if none matched. It does not statically prove that every union member is covered. #### `.otherwise(handler)` ```ts otherwise(handler: (value: T) => R): R; ``` Like a final catch-all case: runs `handler` if no earlier case matched. Unlike `.exhaustive()`, it never throws. ### `P.literal(value)` ```ts const P.literal: ( value: L, ) => Matcher; ``` Matches a single literal with `===` and narrows to its literal type. ### `P.type(type)` ```ts const P.type: ( type: "string" | "number" | "boolean" | "bigint" | "symbol" | "undefined" | "object" | "function", ) => Matcher; ``` Matches a `typeof` result and narrows to the explicitly supplied `T`. `T` is not inferred from the name, so the type parameter and the runtime name must agree. ### `P.when(predicate)` ```ts const P.when: (predicate: (value: unknown) => value is T) => Matcher; ``` Wraps a type guard as a matcher. This is the constructor to prefer when you can express the check as a guard. ### `P.any(predicate)` ```ts const P.any: (predicate: (value: unknown) => boolean) => Matcher; ``` Wraps a boolean predicate and declares the narrowed type `T` yourself. Use it only when a type guard is not expressible; prefer `P.when`. ### `P.shape(shape, refine?)` ```ts const P.shape: ( shape: S, refine?: (value: S) => value is T, ) => Matcher; ``` Matches an object that has every key of `shape`. A shape value that is a `Matcher` is applied; otherwise the value is compared with `===`. Pass `refine` to narrow to `T`; without it, the matched type is `S`. ### Types ```ts interface Matcher { readonly matches: (value: unknown) => value is T; } type Pattern = Matcher; ``` Every pattern constructor returns a `Matcher`. `Pattern` is an alias kept for readability. ## License MIT © 2025 tmu. See [LICENSE](./LICENSE). ## Contributing Contributions are documented in [CONTRIBUTING.md](./CONTRIBUTING.md); the reasons behind the project's decisions, rejected alternatives, and known issues live in [development/](./development/README.md). AI coding agents start at [AGENTS.md](./AGENTS.md).