diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c56704..d1278c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- document the public API in the README: the four factories, when to use each, + what the `W` (widening) suffix means, and examples that bind the handler-map + function once +- add TSDoc to the four public matcher factories + ## [0.8.1] - 2026-09-23 - gate CI at 100% coverage: `test:ci` runs c8 with `--all --100` over `src/`, diff --git a/LICENSE b/LICENSE index 01f2d94..f5ee8b1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 tmu +Copyright (c) 2026 tmu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c6fb26e..ee62d7a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,93 @@ for the design decisions and [Caveats](#caveats) for the limits. ## API -Yet to be implemented +The package exports four factories. Two axes pick one: + +- **Universe** — a _primitive-union_ matcher matches a value that is itself a + finite union (`"yes" | "no"`); a _tagged-union_ matcher matches an object + discriminated by a property (`{ kind: … }`). +- **Return** — the _strict_ variant gives every handler one common return type + `R`; the _widening_ variant (`W`) widens the return value to the union of the + handler returns. + +| Factory | Use when | Return | +| -------------------------------- | ------------------------------------------------------------------------- | --------------------- | +| `getPrimitiveUnionMatcher()` | the value is the union and all handlers return the same type | one common `R` | +| `getPrimitiveUnionMatcherW()` | the value is the union and handlers return different types | union of the handlers | +| `getTaggedUnionMatcher()` | the value is a discriminated object and all handlers return the same type | one common `R` | +| `getTaggedUnionMatcherW()` | the value is a discriminated object and handlers return different types | union of the handlers | + +Bind the function that takes the handler map to a `match…` variable once and +reuse it; the examples below do this, so the builder is allocated once. + +### Primitive-union matchers + +`getPrimitiveUnionMatcher()` 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 matchAnswer = getPrimitiveUnionMatcher<"yes" | "no">(); + +const reply = matchAnswer({ + 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 matchLabel = getPrimitiveUnionMatcher<"yes" | "no" | "maybe">(); + +const label = matchLabel( + { 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()` 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 matchShape = getTaggedUnionMatcher()("kind"); + +const area = matchShape({ + 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 @@ -55,7 +141,7 @@ cover. The type-level cost of supporting open universes is recorded in ## License -MIT © 2025 tmu. See [LICENSE](./LICENSE). +MIT © 2026 tmu. See [LICENSE](./LICENSE). ## Contributing diff --git a/backlog.tasks b/backlog.tasks index 4c74fb7..4eb0e05 100644 --- a/backlog.tasks +++ b/backlog.tasks @@ -8,10 +8,10 @@ Setup: ☐ Split off template into separate package => pi --session 01a07dde-7050-7054-bb36-1606d7eb2bc3 @low v1.0: -☐ API surface is stable and fully typed - ☐ Finalize public exports in `src/index.ts` - ☐ Document all exported types and functions - ☐ Add JSDoc for public APIs +✔ API surface is stable and fully typed @done + ✔ Finalize public exports in `src/index.ts` @done + ✔ Document all exported types and functions @done + ✔ Add JSDoc for public APIs @done ✔ Test coverage meets threshold @done ✔ Achieve 100% branch coverage on `src/primitive-union.ts` @done ✔ Achieve 100% branch coverage on `src/index.ts` @done @@ -28,6 +28,8 @@ Documentation: → previous Examples order: literal/exhaustive, typeof, structural/discriminated unions, when, any ☐ Create `examples/` directory with runnable snippets ☐ Add comparison section vs. other TS pattern-matching libs in Readme.md + ☐ Why do we do this? => exhaustiveness encoded type safe + ☐ Why this form? little syntax, data last, very small, autocomplete, strict typing in the handler; for more features use ts-pattern ☐ Write migration guide for users coming from discriminated unions ☐ Create backlog tasks for implementation ☐ Validate code fences in Markdown (start with README.md) — compile the TypeScript examples against `src/` so the docs cannot drift from the API diff --git a/development/library.md b/development/library.md index fc3a3f6..909db0e 100644 --- a/development/library.md +++ b/development/library.md @@ -5,8 +5,39 @@ user-facing reference is [README § API](../README.md#api). The matchers are implemented in `src/primitive-union.ts` (`getPrimitiveUnionMatcher` / `getPrimitiveUnionMatcherW`) and `src/tagged-union.ts` (`getTaggedUnionMatcher` / -`getTaggedUnionMatcherW`), re-exported from `src/index.ts`; the rest of the -library is placeholder code. +`getTaggedUnionMatcherW`), re-exported from `src/index.ts`; the rest of `src/` +is implementation detail. + +## Public surface + +#### Decision (2026-09) + +`src/index.ts` exports the four factories and nothing else. Every exported +function carries TSDoc; the builder types and the `matcher-shared.ts` +vocabulary stay internal. + +#### Why + +- The factories are the whole contract: a consumer calls one and never needs to + name the builder type it returns. +- The builder interfaces are already the inferred return types, so they travel + into the emitted `.d.ts` regardless. Exporting them would only make them + nameable while freezing the internal `Strict` / `Widening` overload split as + API. +- TSDoc travels into the emitted declarations, so editor hovers and the + published package document the API without a hand-written `.d.ts`. + +#### Rejected + +- **Exporting the builder types** (`PrimitiveUnionMatcher`, …). Nameable, but it + grows the surface for no call-site benefit and pins the `Strict` / `Widening` + split. +- **Exporting the `matcher-shared.ts` vocabulary** (`Matchable`, `UnaryFn`, + `PatternKey`, `Member`, `PatternReturns`, …). They appear in the public + signatures, but a consumer never needs to name them; exporting them would + freeze plumbing as API. +- **A hand-written `.d.ts` or a separate API document.** It would drift from the + implementation; TSDoc is generated from the source. ## Matcher shape diff --git a/src/index.ts b/src/index.ts index 608736e..be33df2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,11 @@ +/** + * The public entry point of `tiny-pattern-ts`. + * + * Exports the four matcher factories and nothing else; the builder types they + * return and the rest of `src/` are implementation detail. + * + * @module + */ export { getPrimitiveUnionMatcher, getPrimitiveUnionMatcherW, diff --git a/src/primitive-union.ts b/src/primitive-union.ts index f3b328b..da6a705 100644 --- a/src/primitive-union.ts +++ b/src/primitive-union.ts @@ -96,9 +96,50 @@ const dispatch = shape as never, ); +/** + * Create a matcher for a finite primitive universe, with one common return + * type. + * + * Use it when the value itself is the union (`"yes" | "no"`) and every handler + * returns the same type. + * + * The returned builder takes a handler map keyed by the members; supplying a + * second fallback argument allows a partial map and receives the unhandled + * remainder. + * + * @typeParam T - The finite universe of primitive members to match. + * @returns A builder for the handler map, or the handler map plus a fallback. + * @example + * const matchAnswer = getPrimitiveUnionMatcher<"yes" | "no">(); + * const describe = matchAnswer({ + * yes: () => "agreed", + * no: () => "declined", + * }); + * describe("yes"); // "agreed" + */ export const getPrimitiveUnionMatcher = < T extends Matchable, >(): PrimitiveUnionMatcherStrict => dispatch; + +/** + * Create a matcher for a finite primitive universe whose return type is the + * union of every handler's return type. + * + * Use it when the value itself is the union (`"yes" | "no"`) and the handlers + * return different types. The `W` (widening) counterpart of + * {@link getPrimitiveUnionMatcher}; the universe constraint and the optional + * fallback are identical. + * + * @typeParam T - The finite universe of primitive members to match. + * @returns A builder for the handler map, or the handler map plus a fallback. + * @example + * const matchReply = getPrimitiveUnionMatcherW<"yes" | "no">(); + * const reply = matchReply({ + * yes: () => 1, + * no: () => "declined", + * }); + * // reply: (shape: "yes" | "no") => number | string + */ export const getPrimitiveUnionMatcherW = < T extends Matchable, >(): PrimitiveUnionMatcherWidening => dispatch; diff --git a/src/tagged-union.ts b/src/tagged-union.ts index b43e9ad..d3a2acd 100644 --- a/src/tagged-union.ts +++ b/src/tagged-union.ts @@ -152,10 +152,56 @@ const dispatch = ); }; +/** + * Create a matcher for a discriminated union, with one common return type. + * + * Use it when the value is an object discriminated by a property + * (`{ kind: "circle" } | { kind: "square" }`) and every handler returns the + * same type. + * + * The first call fixes the union `T`; the returned function takes the + * discriminant property's name (`K`, restricted to properties whose values are + * tags), and that returns the handler-map builder. Supplying a second fallback + * argument to the builder allows a partial map and receives the members whose + * tag was not handled. A `boolean`, `null` or `undefined` tag is keyed by its + * stringified form (`true` -> `"true"`); see README § Caveats. + * + * @typeParam T - The discriminated-union type to match. + * @returns A function that takes the discriminant property's name. + * @example + * type Shape = + * | { kind: "circle"; radius: number } + * | { kind: "square"; side: number }; + * + * const matchShape = getTaggedUnionMatcher()("kind"); + * const area = matchShape({ + * circle: (s) => Math.PI * s.radius ** 2, + * square: (s) => s.side ** 2, + * }); + */ export const getTaggedUnionMatcher = < T extends object, >(): TaggedUnionMatcherFactory => dispatch; +/** + * Create a matcher for a discriminated union whose return type is the union of + * every handler's return type. + * + * Use it when the value is a discriminated object and the handlers return + * different types. The `W` (widening) counterpart of + * {@link getTaggedUnionMatcher}; the curried key step and the optional fallback + * are identical. + * + * @typeParam T - The discriminated-union type to match. + * @returns A function that takes the discriminant property's name. + * @example + * const matchShape = getTaggedUnionMatcherW()("kind"); + * const describe = matchShape({ + * circle: () => "round", + * square: () => 4, + * }); + * // describe: (shape: Shape) => string | number + */ export const getTaggedUnionMatcherW = < T extends object, >(): TaggedUnionMatcherWideningFactory => dispatch;