♻️ Keep the public API to the four factories

The builder types are already the inferred return types and travel into
the emitted .d.ts, so exporting them only made them nameable while
pinning the internal Strict/Widening split as API. Revert the type
exports and the public renames, drop the type-surface test, and document
only the factories in README and development/library.md.
This commit is contained in:
tmu committed 2026-09-23 22:19:02 +00:00
1 parent 9c9468fa3c
commit 5ddbbdc183
7 files changed
+37 -145

No files matched your search

+1 -4
View File
@@ -7,10 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
- export the matcher builder types from the entry point — - add TSDoc to the four public matcher factories
`PrimitiveUnionMatcher` / `PrimitiveUnionMatcherW`, `TaggedUnionMatcher` /
`TaggedUnionMatcherW`, and the tagged-union factory types — and add TSDoc to
every public symbol
- document the public API in the README - document the public API in the README
## [0.8.1] - 2026-09-23 ## [0.8.1] - 2026-09-23
+10 -10
View File
@@ -21,17 +21,17 @@ for the design decisions and [Caveats](#caveats) for the limits.
## API ## API
The package exports four factories and the builder types they return. Each The package exports four factories. Each comes in a _strict_ variant (one common
factory comes in a _strict_ variant (one common return type `R`) and a return type `R`) and a _widening_ variant: the `W` suffix means **widening** —
_widening_ variant: the `W` suffix means **widening** — the return value is the return value is widened from one common `R` to the union of every handler's
widened from one common `R` to the union of every handler's return type. return type.
| Factory | Builder type | Return of the matcher | | Factory | Return of the matcher |
| -------------------------------- | ------------------------------- | -------------------------------- | | -------------------------------- | -------------------------------- |
| `getPrimitiveUnionMatcher<T>()` | `PrimitiveUnionMatcher<T>` | one common `R` | | `getPrimitiveUnionMatcher<T>()` | one common `R` |
| `getPrimitiveUnionMatcherW<T>()` | `PrimitiveUnionMatcherW<T>` | the union of the handler returns | | `getPrimitiveUnionMatcherW<T>()` | the union of the handler returns |
| `getTaggedUnionMatcher<T>()` | `TaggedUnionMatcherFactory<T>` | one common `R` | | `getTaggedUnionMatcher<T>()` | one common `R` |
| `getTaggedUnionMatcherW<T>()` | `TaggedUnionMatcherWFactory<T>` | the union of the handler returns | | `getTaggedUnionMatcherW<T>()` | the union of the handler returns |
### Primitive-union matchers ### Primitive-union matchers
+13 -15
View File
@@ -12,32 +12,30 @@ is implementation detail.
#### Decision (2026-09) #### Decision (2026-09)
`src/index.ts` exports the four factories and the builder types they return: `src/index.ts` exports the four factories and nothing else. Every exported
`PrimitiveUnionMatcher` / `PrimitiveUnionMatcherW`, `TaggedUnionMatcher` / function carries TSDoc; the builder types and the `matcher-shared.ts`
`TaggedUnionMatcherW`, and `TaggedUnionMatcherFactory` / vocabulary stay internal.
`TaggedUnionMatcherWFactory`. Every exported symbol carries TSDoc. The
universe-agnostic pieces in `matcher-shared.ts` stay internal.
#### Why #### Why
- The builder interfaces are already the inferred return types (they appear in - The factories are the whole contract: a consumer calls one and never needs to
the emitted `.d.ts`), so exporting them only makes them nameable — a consumer name the builder type it returns.
can annotate a factory result without restating its structure. - The builder interfaces are already the inferred return types, so they travel
- The `Strict` / `Widening` suffixes were dropped for the public names because into the emitted `.d.ts` regardless. Exporting them would only make them
the factory names already carry the `W` axis; the type and function names line nameable while freezing the internal `Strict` / `Widening` overload split as
up (`getPrimitiveUnionMatcher` → `PrimitiveUnionMatcher`). API.
- TSDoc travels into the emitted declarations, so editor hovers and the - TSDoc travels into the emitted declarations, so editor hovers and the
published package document the API without a hand-written `.d.ts`. published package document the API without a hand-written `.d.ts`.
- `matcher-shared.ts` stays internal: its types are plumbing (`Handlers`,
`Fallback`, `UniverseGate`, …) whose shape follows the implementation, and the
matchers are the stable contract.
#### Rejected #### 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`, - **Exporting the `matcher-shared.ts` vocabulary** (`Matchable`, `UnaryFn`,
`PatternKey`, `Member`, `PatternReturns`, …). They appear in the public `PatternKey`, `Member`, `PatternReturns`, …). They appear in the public
signatures, but a consumer never needs to name them; exporting them would signatures, but a consumer never needs to name them; exporting them would
freeze internals as API. freeze plumbing as API.
- **A hand-written `.d.ts` or a separate API document.** It would drift from the - **A hand-written `.d.ts` or a separate API document.** It would drift from the
implementation; TSDoc is generated from the source. implementation; TSDoc is generated from the source.
-49
View File
@@ -8,18 +8,8 @@ import {
getPrimitiveUnionMatcherW, getPrimitiveUnionMatcherW,
getTaggedUnionMatcher, getTaggedUnionMatcher,
getTaggedUnionMatcherW, getTaggedUnionMatcherW,
type PrimitiveUnionMatcher,
type PrimitiveUnionMatcherW,
type TaggedUnionMatcher,
type TaggedUnionMatcherFactory,
type TaggedUnionMatcherW,
type TaggedUnionMatcherWFactory,
} from "./index.ts"; } from "./index.ts";
interface Shape {
readonly kind: "circle" | "square";
}
// The published entry point is the barrel (`package.json` exports // The published entry point is the barrel (`package.json` exports
// `./dist/index.js`), so every factory must be reachable from here. Importing it // `./dist/index.js`), so every factory must be reachable from here. Importing it
// also loads the module, which is what lets c8's `--all` measure it — see // also loads the module, which is what lets c8's `--all` measure it — see
@@ -35,42 +25,3 @@ test("index: the public entry point re-exports every matcher factory", () => {
expectTypeOf(getTaggedUnionMatcherW).toBeFunction(); expectTypeOf(getTaggedUnionMatcherW).toBeFunction();
assert.equal(typeof getTaggedUnionMatcherW, "function"); assert.equal(typeof getTaggedUnionMatcherW, "function");
}); });
// The builder types are part of the public contract: a consumer annotates a
// factory result with them, so the exported names must match the inferred
// return types.
test("index: the primitive-union factories return the exported builder types", () => {
// Act
const strict = getPrimitiveUnionMatcher<"a" | "b">();
const widening = getPrimitiveUnionMatcherW<"a" | "b">();
// Assert
expectTypeOf(strict).toEqualTypeOf<PrimitiveUnionMatcher<"a" | "b">>();
assert.equal(typeof strict, "function");
expectTypeOf(widening).toEqualTypeOf<PrimitiveUnionMatcherW<"a" | "b">>();
assert.equal(typeof widening, "function");
});
test("index: the tagged-union factories return the exported factory types", () => {
// Act
const strict = getTaggedUnionMatcher<Shape>();
const widening = getTaggedUnionMatcherW<Shape>();
// Assert
expectTypeOf(strict).toEqualTypeOf<TaggedUnionMatcherFactory<Shape>>();
assert.equal(typeof strict, "function");
expectTypeOf(widening).toEqualTypeOf<TaggedUnionMatcherWFactory<Shape>>();
assert.equal(typeof widening, "function");
});
test("index: the tagged-union key step returns the exported builder types", () => {
// Act
const strict = getTaggedUnionMatcher<Shape>()("kind");
const widening = getTaggedUnionMatcherW<Shape>()("kind");
// Assert
expectTypeOf(strict).toEqualTypeOf<TaggedUnionMatcher<Shape, "kind">>();
assert.equal(typeof strict, "function");
expectTypeOf(widening).toEqualTypeOf<TaggedUnionMatcherW<Shape, "kind">>();
assert.equal(typeof widening, "function");
});
+2 -12
View File
@@ -1,8 +1,8 @@
/** /**
* The public entry point of `tiny-pattern-ts`. * The public entry point of `tiny-pattern-ts`.
* *
* Exports the four matcher factories and the builder types they return. * Exports the four matcher factories and nothing else; the builder types they
* Everything else in `src/` is an implementation detail. * return and the rest of `src/` are implementation detail.
* *
* @module * @module
*/ */
@@ -10,17 +10,7 @@ export {
getPrimitiveUnionMatcher, getPrimitiveUnionMatcher,
getPrimitiveUnionMatcherW, getPrimitiveUnionMatcherW,
} from "./primitive-union.ts"; } from "./primitive-union.ts";
export type {
PrimitiveUnionMatcher,
PrimitiveUnionMatcherW,
} from "./primitive-union.ts";
export { export {
getTaggedUnionMatcher, getTaggedUnionMatcher,
getTaggedUnionMatcherW, getTaggedUnionMatcherW,
} from "./tagged-union.ts"; } from "./tagged-union.ts";
export type {
TaggedUnionMatcher,
TaggedUnionMatcherFactory,
TaggedUnionMatcherW,
TaggedUnionMatcherWFactory,
} from "./tagged-union.ts";
+4 -18
View File
@@ -44,14 +44,7 @@ type MustBePartial<T extends Matchable, Handled> =
// Strict returns: one common `R`. Overload order is load-bearing: // Strict returns: one common `R`. Overload order is load-bearing:
// #1 Handlers (first) -> the exhaustive form and the autocomplete popup // #1 Handlers (first) -> the exhaustive form and the autocomplete popup
// #2 Fallback (last) -> accepts a partial handler map plus a fallback // #2 Fallback (last) -> accepts a partial handler map plus a fallback
/** interface PrimitiveUnionMatcherStrict<T extends Matchable> {
* The builder returned by {@link getPrimitiveUnionMatcher}: call it with a
* handler map for every member of `T` to get a strict matcher, or with a partial
* map plus a fallback.
*
* @typeParam T - The finite universe of primitive members to match.
*/
export interface PrimitiveUnionMatcher<T extends Matchable> {
<R>(handlers: Handlers<T, R> & UniverseGate<T>): UnaryFn<T, R>; <R>(handlers: Handlers<T, R> & UniverseGate<T>): UnaryFn<T, R>;
< <
R, R,
@@ -66,14 +59,7 @@ export interface PrimitiveUnionMatcher<T extends Matchable> {
// Widened returns: the union of every handler's return type. `P` is inferred // Widened returns: the union of every handler's return type. `P` is inferred
// from the whole handler map, whose closed constraint supplies the // from the whole handler map, whose closed constraint supplies the
// contextual/autocomplete type. // contextual/autocomplete type.
/** interface PrimitiveUnionMatcherWidening<T extends Matchable> {
* The builder returned by {@link getPrimitiveUnionMatcherW}: like
* {@link PrimitiveUnionMatcher}, but the result's return type is the union of
* every handler's return type instead of one common `R`.
*
* @typeParam T - The finite universe of primitive members to match.
*/
export interface PrimitiveUnionMatcherW<T extends Matchable> {
<P extends Exact<Handlers<T, unknown>, P>>( <P extends Exact<Handlers<T, unknown>, P>>(
handlers: P & UniverseGate<T>, handlers: P & UniverseGate<T>,
): UnaryFn<T, PatternReturns<P>>; ): UnaryFn<T, PatternReturns<P>>;
@@ -130,7 +116,7 @@ const dispatch =
*/ */
export const getPrimitiveUnionMatcher = < export const getPrimitiveUnionMatcher = <
T extends Matchable, T extends Matchable,
>(): PrimitiveUnionMatcher<T> => dispatch; >(): PrimitiveUnionMatcherStrict<T> => dispatch;
/** /**
* Create a matcher for a finite primitive universe whose return type is the * Create a matcher for a finite primitive universe whose return type is the
@@ -151,4 +137,4 @@ export const getPrimitiveUnionMatcher = <
*/ */
export const getPrimitiveUnionMatcherW = < export const getPrimitiveUnionMatcherW = <
T extends Matchable, T extends Matchable,
>(): PrimitiveUnionMatcherW<T> => dispatch; >(): PrimitiveUnionMatcherWidening<T> => dispatch;
+7 -37
View File
@@ -87,15 +87,7 @@ type MustBePartial<T extends object, K extends keyof T, Handled> =
// Strict returns: one common `R`. Overload order is load-bearing: // Strict returns: one common `R`. Overload order is load-bearing:
// #1 Handlers (first) -> the exhaustive form and the autocomplete popup // #1 Handlers (first) -> the exhaustive form and the autocomplete popup
// #2 Fallback (last) -> accepts a partial handler map plus a fallback // #2 Fallback (last) -> accepts a partial handler map plus a fallback
/** interface TaggedUnionMatcherStrict<T extends object, K extends keyof T> {
* The builder returned by the key step of {@link getTaggedUnionMatcher}: call it
* with a handler map for every tag under `K` to get a strict matcher, or with a
* partial map plus a fallback.
*
* @typeParam T - The discriminated-union type to match.
* @typeParam K - The discriminant property of `T`.
*/
export interface TaggedUnionMatcher<T extends object, K extends keyof T> {
<R>(handlers: Handlers<T, K, R> & UniverseGate<Tags<T, K>>): UnaryFn<T, R>; <R>(handlers: Handlers<T, K, R> & UniverseGate<Tags<T, K>>): UnaryFn<T, R>;
< <
R, R,
@@ -112,15 +104,7 @@ export interface TaggedUnionMatcher<T extends object, K extends keyof T> {
// Widened returns: the union of every handler's return type. `P` is inferred // Widened returns: the union of every handler's return type. `P` is inferred
// from the whole handler map, whose closed constraint supplies the // from the whole handler map, whose closed constraint supplies the
// contextual/autocomplete type. // contextual/autocomplete type.
/** interface TaggedUnionMatcherWidening<T extends object, K extends keyof T> {
* The builder returned by the key step of {@link getTaggedUnionMatcherW}: like
* {@link TaggedUnionMatcher}, but the result's return type is the union of every
* handler's return type instead of one common `R`.
*
* @typeParam T - The discriminated-union type to match.
* @typeParam K - The discriminant property of `T`.
*/
export interface TaggedUnionMatcherW<T extends object, K extends keyof T> {
<P extends Exact<Handlers<T, K, unknown>, P>>( <P extends Exact<Handlers<T, K, unknown>, P>>(
handlers: P & UniverseGate<Tags<T, K>>, handlers: P & UniverseGate<Tags<T, K>>,
): UnaryFn<T, PatternReturns<P>>; ): UnaryFn<T, PatternReturns<P>>;
@@ -137,29 +121,15 @@ export interface TaggedUnionMatcherW<T extends object, K extends keyof T> {
// The key-taking step of the curried factory. Naming it lets the factory return // The key-taking step of the curried factory. Naming it lets the factory return
// `dispatch` directly, the tacit twin of the primitive-union factory's bare // `dispatch` directly, the tacit twin of the primitive-union factory's bare
// `=> dispatch`. // `=> dispatch`.
/** type TaggedUnionMatcherFactory<T extends object> = <K extends Discriminated<T>>(
* The function returned by {@link getTaggedUnionMatcher}: call it with the
* discriminant property's name to get the handler-map builder.
*
* @typeParam T - The discriminated-union type to match.
*/
export type TaggedUnionMatcherFactory<T extends object> = <
K extends Discriminated<T>,
>(
k: K, k: K,
) => TaggedUnionMatcher<T, K>; ) => TaggedUnionMatcherStrict<T, K>;
/** type TaggedUnionMatcherWideningFactory<T extends object> = <
* The function returned by {@link getTaggedUnionMatcherW}: call it with the
* discriminant property's name to get the widening handler-map builder.
*
* @typeParam T - The discriminated-union type to match.
*/
export type TaggedUnionMatcherWFactory<T extends object> = <
K extends Discriminated<T>, K extends Discriminated<T>,
>( >(
k: K, k: K,
) => TaggedUnionMatcherW<T, K>; ) => TaggedUnionMatcherWidening<T, K>;
const dispatch = const dispatch =
(k: PropertyKey) => (k: PropertyKey) =>
@@ -227,4 +197,4 @@ export const getTaggedUnionMatcher = <
*/ */
export const getTaggedUnionMatcherW = < export const getTaggedUnionMatcherW = <
T extends object, T extends object,
>(): TaggedUnionMatcherWFactory<T> => dispatch; >(): TaggedUnionMatcherWideningFactory<T> => dispatch;