🔀 Merge feature/api-surface into main
This commit is contained in:
commit
d1963b0329
8 files changed
+228
-9
No files matched your search
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.8.1] - 2026-09-23
|
||||||
|
|
||||||
- gate CI at 100% coverage: `test:ci` runs c8 with `--all --100` over `src/`,
|
- gate CI at 100% coverage: `test:ci` runs c8 with `--all --100` over `src/`,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 tmu
|
Copyright (c) 2026 tmu
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -21,7 +21,93 @@ for the design decisions and [Caveats](#caveats) for the limits.
|
|||||||
|
|
||||||
## API
|
## 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<T>()` | the value is the union and all handlers return the same type | one common `R` |
|
||||||
|
| `getPrimitiveUnionMatcherW<T>()` | the value is the union and handlers return different types | union of the handlers |
|
||||||
|
| `getTaggedUnionMatcher<T>()` | the value is a discriminated object and all handlers return the same type | one common `R` |
|
||||||
|
| `getTaggedUnionMatcherW<T>()` | 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<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 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<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 matchShape = getTaggedUnionMatcher<Shape>()("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
|
## Caveats
|
||||||
|
|
||||||
@@ -55,7 +141,7 @@ cover. The type-level cost of supporting open universes is recorded in
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT © 2025 tmu. See [LICENSE](./LICENSE).
|
MIT © 2026 tmu. See [LICENSE](./LICENSE).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -8,10 +8,10 @@ Setup:
|
|||||||
☐ Split off template into separate package => pi --session 01a07dde-7050-7054-bb36-1606d7eb2bc3 @low
|
☐ Split off template into separate package => pi --session 01a07dde-7050-7054-bb36-1606d7eb2bc3 @low
|
||||||
|
|
||||||
v1.0:
|
v1.0:
|
||||||
☐ API surface is stable and fully typed
|
✔ API surface is stable and fully typed @done
|
||||||
☐ Finalize public exports in `src/index.ts`
|
✔ Finalize public exports in `src/index.ts` @done
|
||||||
☐ Document all exported types and functions
|
✔ Document all exported types and functions @done
|
||||||
☐ Add JSDoc for public APIs
|
✔ Add JSDoc for public APIs @done
|
||||||
✔ Test coverage meets threshold @done
|
✔ Test coverage meets threshold @done
|
||||||
✔ Achieve 100% branch coverage on `src/primitive-union.ts` @done
|
✔ Achieve 100% branch coverage on `src/primitive-union.ts` @done
|
||||||
✔ Achieve 100% branch coverage on `src/index.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
|
→ previous Examples order: literal/exhaustive, typeof, structural/discriminated unions, when, any
|
||||||
☐ Create `examples/` directory with runnable snippets
|
☐ Create `examples/` directory with runnable snippets
|
||||||
☐ Add comparison section vs. other TS pattern-matching libs in Readme.md
|
☐ 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
|
☐ Write migration guide for users coming from discriminated unions
|
||||||
☐ Create backlog tasks for implementation
|
☐ 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
|
☐ Validate code fences in Markdown (start with README.md) — compile the TypeScript examples against `src/` so the docs cannot drift from the API
|
||||||
|
|||||||
+33
-2
@@ -5,8 +5,39 @@ user-facing reference is [README § API](../README.md#api).
|
|||||||
|
|
||||||
The matchers are implemented in `src/primitive-union.ts` (`getPrimitiveUnionMatcher` /
|
The matchers are implemented in `src/primitive-union.ts` (`getPrimitiveUnionMatcher` /
|
||||||
`getPrimitiveUnionMatcherW`) and `src/tagged-union.ts` (`getTaggedUnionMatcher` /
|
`getPrimitiveUnionMatcherW`) and `src/tagged-union.ts` (`getTaggedUnionMatcher` /
|
||||||
`getTaggedUnionMatcherW`), re-exported from `src/index.ts`; the rest of the
|
`getTaggedUnionMatcherW`), re-exported from `src/index.ts`; the rest of `src/`
|
||||||
library is placeholder code.
|
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
|
## Matcher shape
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
export {
|
||||||
getPrimitiveUnionMatcher,
|
getPrimitiveUnionMatcher,
|
||||||
getPrimitiveUnionMatcherW,
|
getPrimitiveUnionMatcherW,
|
||||||
|
|||||||
@@ -96,9 +96,50 @@ const dispatch =
|
|||||||
shape as never,
|
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 = <
|
export const getPrimitiveUnionMatcher = <
|
||||||
T extends Matchable,
|
T extends Matchable,
|
||||||
>(): PrimitiveUnionMatcherStrict<T> => dispatch;
|
>(): PrimitiveUnionMatcherStrict<T> => 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 = <
|
export const getPrimitiveUnionMatcherW = <
|
||||||
T extends Matchable,
|
T extends Matchable,
|
||||||
>(): PrimitiveUnionMatcherWidening<T> => dispatch;
|
>(): PrimitiveUnionMatcherWidening<T> => dispatch;
|
||||||
@@ -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<Shape>()("kind");
|
||||||
|
* const area = matchShape({
|
||||||
|
* circle: (s) => Math.PI * s.radius ** 2,
|
||||||
|
* square: (s) => s.side ** 2,
|
||||||
|
* });
|
||||||
|
*/
|
||||||
export const getTaggedUnionMatcher = <
|
export const getTaggedUnionMatcher = <
|
||||||
T extends object,
|
T extends object,
|
||||||
>(): TaggedUnionMatcherFactory<T> => dispatch;
|
>(): TaggedUnionMatcherFactory<T> => 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<Shape>()("kind");
|
||||||
|
* const describe = matchShape({
|
||||||
|
* circle: () => "round",
|
||||||
|
* square: () => 4,
|
||||||
|
* });
|
||||||
|
* // describe: (shape: Shape) => string | number
|
||||||
|
*/
|
||||||
export const getTaggedUnionMatcherW = <
|
export const getTaggedUnionMatcherW = <
|
||||||
T extends object,
|
T extends object,
|
||||||
>(): TaggedUnionMatcherWideningFactory<T> => dispatch;
|
>(): TaggedUnionMatcherWideningFactory<T> => dispatch;
|
||||||
Reference in new issue
Block a user