Export the matcher builder types from the barrel and rename them off the internal Strict/Widening suffixes, so the type a factory returns is nameable: PrimitiveUnionMatcher/W, TaggedUnionMatcher/W, and the tagged-union factory types. Add TSDoc to every public symbol — the declarations carry it into the published package — and pin the exported names in src/index.test.ts. Document the API in README and record the surface decision (and the rejected matcher-shared export) in development/library.md.
138 lines
5.9 KiB
Markdown
138 lines
5.9 KiB
Markdown
# tiny-pattern-ts
|
|
|
|
Pattern matching for TypeScript/ESM environments (F#-style, not regex).
|
|
|
|
## Description
|
|
|
|
`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 [Caveats](#caveats) for the limits.
|
|
|
|
## 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).
|
|
|
|
## API
|
|
|
|
The package exports four factories and the builder types they return. Each
|
|
factory comes in a _strict_ variant (one common return type `R`) and a
|
|
_widening_ variant (the union of every handler's return type):
|
|
|
|
| Factory | Builder type | Return of the matcher |
|
|
| -------------------------------- | ------------------------------- | -------------------------------- |
|
|
| `getPrimitiveUnionMatcher<T>()` | `PrimitiveUnionMatcher<T>` | one common `R` |
|
|
| `getPrimitiveUnionMatcherW<T>()` | `PrimitiveUnionMatcherW<T>` | the union of the handler returns |
|
|
| `getTaggedUnionMatcher<T>()` | `TaggedUnionMatcherFactory<T>` | one common `R` |
|
|
| `getTaggedUnionMatcherW<T>()` | `TaggedUnionMatcherWFactory<T>` | the union of the handler returns |
|
|
|
|
### Primitive-union matchers
|
|
|
|
`getPrimitiveUnionMatcher<T>()` takes the finite universe `T` and returns a
|
|
builder. Calling the builder with a handler map keyed by `T`'s members returns a
|
|
matcher: a function from `T` to the common return type.
|
|
|
|
```ts
|
|
import { getPrimitiveUnionMatcher } from "tiny-pattern-ts";
|
|
|
|
const reply = getPrimitiveUnionMatcher<"yes" | "no">()({
|
|
yes: () => "agreed",
|
|
no: () => "declined",
|
|
});
|
|
|
|
reply("yes"); // "agreed"
|
|
```
|
|
|
|
Add a fallback as the second argument to leave members unhandled; the fallback
|
|
receives the remainder:
|
|
|
|
```ts
|
|
const label = getPrimitiveUnionMatcher<"yes" | "no" | "maybe">()(
|
|
{ yes: () => "agreed", no: () => "declined" },
|
|
(other) => `not sure: ${other}`, // other: "maybe"
|
|
);
|
|
```
|
|
|
|
`getPrimitiveUnionMatcherW` is the same builder, but the matcher's return type
|
|
is the union of the handler return types rather than one common `R`.
|
|
|
|
### Tagged-union matchers
|
|
|
|
`getTaggedUnionMatcher<T>()` takes a discriminated union `T`. The returned
|
|
function takes the discriminant property's name and returns the handler-map
|
|
builder, keyed by that property's tags.
|
|
|
|
```ts
|
|
import { getTaggedUnionMatcher } from "tiny-pattern-ts";
|
|
|
|
type Shape =
|
|
{ kind: "circle"; radius: number } | { kind: "square"; side: number };
|
|
|
|
const area = getTaggedUnionMatcher<Shape>()("kind")({
|
|
circle: (s) => Math.PI * s.radius ** 2,
|
|
square: (s) => s.side ** 2,
|
|
});
|
|
|
|
area({ kind: "circle", radius: 2 });
|
|
```
|
|
|
|
`getTaggedUnionMatcherW` is the widening counterpart, exactly as in the
|
|
primitive-union pair. The discriminant key is restricted to properties whose
|
|
values are tags; see [Caveats](#caveats) for the supported tags and the
|
|
`boolean` / `null` / `undefined` key projection.
|
|
|
|
### The universe
|
|
|
|
The primitive universe `T` must be a finite union of literals with no
|
|
value/stringification collision. A broad member (`string`, `number`, a template
|
|
literal) or a colliding pair (`true | "true"`, `1 | "1"`) is rejected at the
|
|
factory. The reasons and the rejected alternatives are in
|
|
[development/library.md](./development/library.md).
|
|
|
|
## Caveats
|
|
|
|
- **Only finite universes are supported.** The factory must be given a finite
|
|
union of literals; `string`, `number` and template literals are rejected. This
|
|
is what lets the exhaustive overload be proven, so the runtime `dispatch`
|
|
throw stays unreachable through the typed API.
|
|
- **A value and its stringification must not both be present.** Object keys
|
|
stringify, so a universe containing both a member and the string it
|
|
stringifies to — `1 | "1"`, `true | "true"`, `null | "null"` — is rejected at
|
|
the factory. Either form alone is fine, and one value's string form may
|
|
coexist with a _different_ value's bare form (`"true" | false`).
|
|
- **`symbol` and `bigint` are not supported.** A `symbol` brand is a
|
|
compile-time phantom with nothing to match at runtime, and a `bigint` is not a
|
|
valid property key; neither satisfies the matcher's universe constraint.
|
|
- **`NaN` and `-0` cannot be matched specifically.** They have no literal type,
|
|
so both stay part of `number`.
|
|
|
|
### Why open universes are rejected
|
|
|
|
An open universe — one carrying a broad member, as in
|
|
`type Units = "s" | "ms" | "min" | (string & {})` — is not a dispatch concern.
|
|
If the values arrive from outside the program, parse them at the boundary down
|
|
to a finite union and match the narrowed result; the openness never reaches the
|
|
matcher. If the domain is genuinely extensible, the right shape is a runtime
|
|
`Map` of handlers, where "no handler" is a lookup, not a pattern. Either way an
|
|
open matcher would abandon the one guarantee this library exists to give —
|
|
provable exhaustiveness — to automate what a `switch` and a default arm already
|
|
cover. The type-level cost of supporting open universes is recorded in
|
|
[development/library.md](./development/library.md#supported-universes).
|
|
|
|
## 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).
|