From a3eb6183af3e34422f8bff6ed1d4010b98529d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Wed, 16 Sep 2026 21:57:55 +0000 Subject: [PATCH] :fire: Remove the match and pattern example modules They were the template's demo API, not the library's real surface. Drop them, their README walkthrough, and the tooling note that cited their disable directives, and point index.ts at the primitive matchers that remain. --- README.md | 150 ++--------------------------------------- development/tooling.md | 4 +- src/index.ts | 8 ++- src/match.ts | 62 ----------------- src/pattern.ts | 105 ----------------------------- 5 files changed, 14 insertions(+), 315 deletions(-) delete mode 100644 src/match.ts delete mode 100644 src/pattern.ts diff --git a/README.md b/README.md index 63dfa9b..5e86b41 100644 --- a/README.md +++ b/README.md @@ -2,34 +2,14 @@ Pattern matching for TypeScript/ESM environments (F#-style, not regex). -## Synopsis - -```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" -``` - ## 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. +`tiny-pattern-ts` brings F#-style pattern matching to TypeScript. 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, and the type-level +contract is the feature — see [development/library.md](./development/library.md) +for the design decisions and the known limitations. ## Requirements @@ -39,124 +19,6 @@ the known limitations. both resolve on TS >= 5.0 in `node10` / `node16` / `nodenext` / `bundler`. - The package is **ESM-only** (no CommonJS shim). -## 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 Yet to be implemented diff --git a/development/tooling.md b/development/tooling.md index d07e768..d1ab59f 100644 --- a/development/tooling.md +++ b/development/tooling.md @@ -105,8 +105,8 @@ Type-aware oxlint is enabled via `options.typeAware: true` in `.oxlintrc.json` #### Decision (2026-09) A type-aware rule that false-positives is silenced with a source-level -`oxlint-disable` directive (see `src/pattern.ts`, `src/match.ts`, -`src/index.test.ts`), not by turning the rule off in `.oxlintrc.json`. +`oxlint-disable` directive (see `src/primitive.ts`, +`src/primitive.test.ts`), not by turning the rule off in `.oxlintrc.json`. #### Why diff --git a/src/index.ts b/src/index.ts index 20f0ec7..096a964 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,6 @@ -export { match, P } from "./match.ts"; -export type { Matcher, Pattern } from "./pattern.ts"; +export { + getPrimitiveUnionMatcher, + getPrimitiveUnionMatcherPartial, + getPrimitiveUnionMatcherPartialW, + getPrimitiveUnionMatcherW, +} from "./primitive.ts"; diff --git a/src/match.ts b/src/match.ts deleted file mode 100644 index ae9672f..0000000 --- a/src/match.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { P, type Matcher, type Pattern } from "./pattern.ts"; - -type Cases = readonly (readonly [Matcher, (value: unknown) => R])[]; - -interface MatchBuilder { - with( - pattern: Matcher, - handler: (value: U) => V, - ): MatchBuilder; - exhaustive(): R; - otherwise(handler: (value: T) => R): R; -} - -const buildMatch = (value: T, cases: Cases): MatchBuilder => { - const apply = (): R | undefined => { - for (const [matcher, handler] of cases) { - if (matcher.matches(value)) { - return handler(value); - } - } - return undefined; - }; - - const builder = { - with( - pattern: Matcher, - handler: (value: U) => V, - ): MatchBuilder { - const nextCases: Cases = [ - ...cases, - // oxlint-disable-next-line typescript/no-unsafe-type-assertion - [pattern, handler as (value: unknown) => R | V], - ]; - return buildMatch(value, nextCases); - }, - exhaustive(): R { - const result = apply(); - if (result === undefined) { - throw new Error( - "tiny-pattern-ts: match.exhaustive() called with no matching case", - ); - } - return result; - }, - otherwise(handler: (value: T) => R): R { - for (const [matcher, run] of cases) { - if (matcher.matches(value)) { - return run(value); - } - } - return handler(value); - }, - }; - - return builder; -}; - -export const match = (value: T): MatchBuilder => - buildMatch(value, []); - -export type { Matcher, Pattern }; -export { P }; diff --git a/src/pattern.ts b/src/pattern.ts deleted file mode 100644 index e7b9e00..0000000 --- a/src/pattern.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Pattern matching primitives. Each constructor returns a lightweight - * matcher object whose `matches` method returns a type guard. - */ - -export interface Matcher { - readonly matches: (value: unknown) => value is T; -} - -const literalMatcher = < - const L extends string | number | boolean | null | undefined, ->( - value: L, -): Matcher => ({ - matches: (candidate): candidate is L => candidate === value, -}); -const typeMatcher = ( - type: - | "string" - | "number" - | "boolean" - | "bigint" - | "symbol" - | "undefined" - | "object" - | "function", - ): Matcher => { - const matches = (value: unknown): value is T => { - if (type === "undefined") { - return value === undefined; - } - if (type === "object") { - return ( - (typeof value === "object" && value !== null) || - typeof value === "function" - ); - } - return typeof value === type; - }; - return { matches }; - }, - whenMatcher = ( - predicate: (value: unknown) => value is T, - ): Matcher => ({ - matches: predicate, - }), - whenMatcherAny = ( - predicate: (value: unknown) => boolean, - ): Matcher => ({ - matches: (value: unknown): value is T => predicate(value), - }), - isNestedMatcher = (expected: unknown): expected is Matcher => - typeof expected === "object" && - expected !== null && - "matches" in expected, - // oxlint-disable-next-line typescript/no-unnecessary-type-parameters - keysMatch = (shape: S, candidate: object): boolean => { - // oxlint-disable-next-line typescript/no-unsafe-type-assertion - for (const key of Object.keys(shape) as (keyof S)[]) { - if (!(key in candidate)) { - return false; - } - // oxlint-disable-next-line typescript/no-unsafe-type-assertion - const expected = shape[key], - // oxlint-disable-next-line typescript/no-unsafe-type-assertion - actual = candidate[key as keyof object]; - if (isNestedMatcher(expected)) { - if (!expected.matches(actual)) { - return false; - } - } else if (actual !== expected) { - return false; - } - } - return true; - }, - structuralMatcher = ( - shape: S, - refine?: (value: S) => value is T, - ): Matcher => ({ - matches: (value: unknown): value is T => { - if (typeof value !== "object" || value === null) { - return false; - } - // oxlint-disable-next-line typescript/no-unsafe-type-assertion - const candidate = value as S; - if (!keysMatch(shape, candidate)) { - return false; - } - if (refine && !refine(candidate)) { - return false; - } - return true; - }, - }); - -export const P = { - literal: literalMatcher, - type: typeMatcher, - when: whenMatcher, - any: whenMatcherAny, - shape: structuralMatcher, -} as const; - -export type Pattern = Matcher;