✨ Finalize and document the public API surface

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.
This commit is contained in:
tmu committed 2026-09-23 21:39:15 +00:00
1 parent acf9d06ddd
commit f485b1bae2
8 files changed
+317 -20

No files matched your search

+49
View File
@@ -8,8 +8,18 @@ import {
getPrimitiveUnionMatcherW,
getTaggedUnionMatcher,
getTaggedUnionMatcherW,
type PrimitiveUnionMatcher,
type PrimitiveUnionMatcherW,
type TaggedUnionMatcher,
type TaggedUnionMatcherFactory,
type TaggedUnionMatcherW,
type TaggedUnionMatcherWFactory,
} from "./index.ts";
interface Shape {
readonly kind: "circle" | "square";
}
// The published entry point is the barrel (`package.json` exports
// `./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
@@ -25,3 +35,42 @@ test("index: the public entry point re-exports every matcher factory", () => {
expectTypeOf(getTaggedUnionMatcherW).toBeFunction();
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");
});
+18
View File
@@ -1,8 +1,26 @@
/**
* The public entry point of `tiny-pattern-ts`.
*
* Exports the four matcher factories and the builder types they return.
* Everything else in `src/` is an implementation detail.
*
* @module
*/
export {
getPrimitiveUnionMatcher,
getPrimitiveUnionMatcherW,
} from "./primitive-union.ts";
export type {
PrimitiveUnionMatcher,
PrimitiveUnionMatcherW,
} from "./primitive-union.ts";
export {
getTaggedUnionMatcher,
getTaggedUnionMatcherW,
} from "./tagged-union.ts";
export type {
TaggedUnionMatcher,
TaggedUnionMatcherFactory,
TaggedUnionMatcherW,
TaggedUnionMatcherWFactory,
} from "./tagged-union.ts";
+54 -4
View File
@@ -44,7 +44,14 @@ type MustBePartial<T extends Matchable, Handled> =
// Strict returns: one common `R`. Overload order is load-bearing:
// #1 Handlers (first) -> the exhaustive form and the autocomplete popup
// #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,
@@ -59,7 +66,14 @@ interface PrimitiveUnionMatcherStrict<T extends Matchable> {
// Widened returns: the union of every handler's return type. `P` is inferred
// from the whole handler map, whose closed constraint supplies the
// 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>>(
handlers: P & UniverseGate<T>,
): UnaryFn<T, PatternReturns<P>>;
@@ -96,9 +110,45 @@ const dispatch =
shape as never,
);
/**
* Create a matcher for a finite primitive universe, with one common return type.
*
* The universe `T` must be a finite union of literals with no
* value/stringification collision: broad members (`string`, `number`, template
* literals) and `true | "true"` / `1 | "1"` are rejected at the call site. 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 describe = getPrimitiveUnionMatcher<"yes" | "no">()({
* yes: () => "agreed",
* no: () => "declined",
* });
* describe("yes"); // "agreed"
*/
export const getPrimitiveUnionMatcher = <
T extends Matchable,
>(): PrimitiveUnionMatcherStrict<T> => dispatch;
>(): PrimitiveUnionMatcher<T> => dispatch;
/**
* Create a matcher for a finite primitive universe whose return type is the
* union of every handler's return type.
*
* The widening counterpart of {@link getPrimitiveUnionMatcher}: use it when the
* handlers return different types and the union, not one common `R`, is wanted.
* 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 reply = getPrimitiveUnionMatcherW<"yes" | "no">()({
* yes: () => 1,
* no: () => "declined",
* });
* // reply: (shape: "yes" | "no") => number | string
*/
export const getPrimitiveUnionMatcherW = <
T extends Matchable,
>(): PrimitiveUnionMatcherWidening<T> => dispatch;
>(): PrimitiveUnionMatcherW<T> => dispatch;
+78 -9
View File
@@ -87,7 +87,15 @@ type MustBePartial<T extends object, K extends keyof T, Handled> =
// Strict returns: one common `R`. Overload order is load-bearing:
// #1 Handlers (first) -> the exhaustive form and the autocomplete popup
// #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,
@@ -104,7 +112,15 @@ interface TaggedUnionMatcherStrict<T extends object, K extends keyof T> {
// Widened returns: the union of every handler's return type. `P` is inferred
// from the whole handler map, whose closed constraint supplies the
// 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>>(
handlers: P & UniverseGate<Tags<T, K>>,
): UnaryFn<T, PatternReturns<P>>;
@@ -121,15 +137,29 @@ interface TaggedUnionMatcherWidening<T extends object, K extends keyof T> {
// 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`.
type TaggedUnionMatcherFactory<T extends object> = <K extends Discriminated<T>>(
k: K,
) => TaggedUnionMatcherStrict<T, K>;
type TaggedUnionMatcherWideningFactory<T extends object> = <
/**
* 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,
) => TaggedUnionMatcherWidening<T, K>;
) => TaggedUnionMatcher<T, K>;
/**
* 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: K,
) => TaggedUnionMatcherW<T, K>;
const dispatch =
(k: PropertyKey) =>
@@ -152,10 +182,49 @@ const dispatch =
);
};
/**
* Create a matcher for a discriminated union, with one common return 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 area = getTaggedUnionMatcher<Shape>()("kind")({
* circle: (s) => Math.PI * s.radius ** 2,
* square: (s) => s.side ** 2,
* });
*/
export const getTaggedUnionMatcher = <
T extends object,
>(): TaggedUnionMatcherFactory<T> => dispatch;
/**
* Create a matcher for a discriminated union whose return type is the union of
* every handler's return type.
*
* The widening counterpart of {@link getTaggedUnionMatcher}: use it when the
* handlers return different types and the union, not one common `R`, is wanted.
* 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 describe = getTaggedUnionMatcherW<Shape>()("kind")({
* circle: () => "round",
* square: () => 4,
* });
* // describe: (shape: Shape) => string | number
*/
export const getTaggedUnionMatcherW = <
T extends object,
>(): TaggedUnionMatcherWideningFactory<T> => dispatch;
>(): TaggedUnionMatcherWFactory<T> => dispatch;