From 5e7d40b013a7de189448508be689116a8f9977c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Tue, 15 Sep 2026 21:46:41 +0000 Subject: [PATCH] :memo: Improve docs after split --- CONTRIBUTING.md | 4 +- README.md | 123 +++----------------------------------- development/ci.md | 4 +- development/library.md | 102 +------------------------------ development/publishing.md | 6 +- development/tooling.md | 6 +- 6 files changed, 17 insertions(+), 228 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4d86df..04cdfb6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ the rules so agents and humans don't diverge. 1. Clone the repository. 2. Install Node.js >= 26 — see [.node-version](./.node-version); the exact pinned version is what CI and the runner image use. -3. `npm install`. +3. `npm ci`. 4. `npm run setup` — the one-time clone configuration (currently registers the commit-message template). @@ -20,7 +20,7 @@ the rules so agents and humans don't diverge. - **Build:** `npm run build` - **Test:** `npm run test`, `npm run test:ci` -- **Watch:** `npm run watch` +- **Watch:** `npm run watch` - re-runs tests on file save, humans only - **Checks:** `npm run check`, `npm run fix` - **Verify:** `npm run verify` — the definition of done - **Maintenance:** `npm run maintain` — advisory only diff --git a/README.md b/README.md index 681a3de..63dfa9b 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,6 @@ Pattern matching for TypeScript/ESM environments (F#-style, not regex). ## Synopsis -```sh -npm install tiny-pattern-ts -``` - ```ts import { match, P } from "tiny-pattern-ts"; @@ -20,14 +16,6 @@ const reply = (answer: "yes" | "no") => reply("yes"); // "agreed" ``` -### 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). - ## Description `tiny-pattern-ts` gives TypeScript the shape of F#-style pattern matching: @@ -43,6 +31,14 @@ The type-level contract is the feature — see [development/library.md](./development/library.md) for the design decisions and the known limitations. +## 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). + ## Examples ### Literal matching and `exhaustive()` @@ -163,108 +159,7 @@ const firstNumber = (items: readonly unknown[]): number | undefined => ## API -### `match(value)` - -```ts -const match: (value: T) => MatchBuilder; -``` - -Starts a matching chain for `value`. The builder is immutable: every `.with` -returns a new builder, so a partially built chain can be reused. - -#### `.with(pattern, handler)` - -```ts -with(pattern: Matcher, handler: (value: U) => V): MatchBuilder; -``` - -Adds a case. `handler` receives the value narrowed to `U`, and its return type -`V` is added to the builder's result union `R`. A pattern whose narrowed type is -not assignable to the matched value's type is a compile error. - -#### `.exhaustive()` - -```ts -exhaustive(): R; -``` - -Returns the result of the first matching case. Throws -`tiny-pattern-ts: match.exhaustive() called with no matching case` if none -matched. It does not statically prove that every union member is covered. - -#### `.otherwise(handler)` - -```ts -otherwise(handler: (value: T) => R): R; -``` - -Like a final catch-all case: runs `handler` if no earlier case matched. Unlike -`.exhaustive()`, it never throws. - -### `P.literal(value)` - -```ts -const P.literal: ( - value: L, -) => Matcher; -``` - -Matches a single literal with `===` and narrows to its literal type. - -### `P.type(type)` - -```ts -const P.type: ( - type: "string" | "number" | "boolean" | "bigint" | "symbol" | "undefined" | "object" | "function", -) => Matcher; -``` - -Matches a `typeof` result and narrows to the explicitly supplied `T`. `T` is not -inferred from the name, so the type parameter and the runtime name must agree. - -### `P.when(predicate)` - -```ts -const P.when: (predicate: (value: unknown) => value is T) => Matcher; -``` - -Wraps a type guard as a matcher. This is the constructor to prefer when you can -express the check as a guard. - -### `P.any(predicate)` - -```ts -const P.any: (predicate: (value: unknown) => boolean) => Matcher; -``` - -Wraps a boolean predicate and declares the narrowed type `T` yourself. Use it -only when a type guard is not expressible; prefer `P.when`. - -### `P.shape(shape, refine?)` - -```ts -const P.shape: ( - shape: S, - refine?: (value: S) => value is T, -) => Matcher; -``` - -Matches an object that has every key of `shape`. A shape value that is a -`Matcher` is applied; otherwise the value is compared with `===`. Pass `refine` -to narrow to `T`; without it, the matched type is `S`. - -### Types - -```ts -interface Matcher { - readonly matches: (value: unknown) => value is T; -} - -type Pattern = Matcher; -``` - -Every pattern constructor returns a `Matcher`. `Pattern` is an alias kept -for readability. +Yet to be implemented ## License diff --git a/development/ci.md b/development/ci.md index cc373df..374363e 100644 --- a/development/ci.md +++ b/development/ci.md @@ -36,8 +36,8 @@ overlaid at the exact `/opt/hostedtoolcache` layout `actions/setup-node` probes. #### Rejected - Downloading Node in every job — the ~50 MB fetch was the original problem. -- Gitea Pages / Codecov for coverage — neither was confirmed available or wanted - (see [Coverage serving](#coverage-serving)). +- Caching Proxy (Squid or similar) — adds complexity to global setup +- Mounting the tool cache - No invalidation will fill the cache with stale versions ## Bumping Node diff --git a/development/library.md b/development/library.md index a0818c3..280e467 100644 --- a/development/library.md +++ b/development/library.md @@ -3,104 +3,4 @@ The type-level design of the public API and the limitations it carries. The user-facing reference is [README § API](../README.md#api). -## A type guard is the single primitive - -#### Decision (2026-09) - -Every pattern constructor returns a `Matcher` whose only member is -`matches: (value: unknown) => value is T`; `Pattern` is an alias. - -#### Why - -- A type guard is the one TypeScript construct that both narrows in an `if` and - composes into a chain, so the library needs no DSL and no transpiler. -- Because `matches` narrows, `match(value).with(pattern, handler)` types the - handler with no cast and no runtime tag. -- `Pattern` is an alias, so either name is the same type. - -## The builder is immutable - -#### Decision (2026-09) - -`.with` returns a new builder rather than mutating the current one. - -#### Why - -- A partially built chain can be stored and reused without one call site - affecting another. -- `.with` widens the result type to `R | V`, which cannot be represented by a - value that changes type in place. - -## `exhaustive()` is a runtime check - -#### Decision (2026-09) - -`.exhaustive()` throws when no case matched; it does not statically prove that -every member of the input union has a case. - -#### Why - -- Proving coverage through a fluent chain would need a compiler plugin or a - builder that tracks an uncovered-member set — out of proportion to this - library's size. -- The union of handler return types is still type-safe; only the chain's - completeness is unchecked. - -#### Known issue - -- A missing case is a runtime `Error`, not a compile error. A caller wants - either an `.otherwise(...)` (which never throws) or to prove coverage - themselves. Tracking uncovered members is a possible future change. - -## `P.type` takes the type as a parameter - -#### Decision (2026-09) - -`P.type(name)` requires the caller to supply `T`; it is not inferred from the -`typeof` string. - -#### Why - -- A runtime string cannot carry a TypeScript type; the caller pairs the two, and - the compiler only checks that `T` is used consistently afterwards. - -#### Known issue - -- `T` and the runtime `name` can disagree (`P.type("string")` compiles), - because nothing ties them together. Prefer `P.when` with a real type guard. A - name-to-type conditional mapping is a possible future change. - -## `P.shape` narrows only through `refine` - -#### Decision (2026-09) - -`P.shape(shape, refine?)` returns `Matcher`, defaulting to `Matcher` -without a `refine`. - -#### Why - -- The shape's values may be matchers, so `S` describes the check, not the value; - the `refine` guard is the explicit point where the value narrows to `T`. -- Keys are checked with `in` rather than requiring an exact match, so a shape can - match a wider object — which is how discriminated unions are handled. - -#### Known issue - -- Without `refine` a discriminated-union case does not narrow, so `P.shape` - alone is easy to misuse. Prefer `P.when` when a guard is already available. - -## `P.any` exists for non-guard predicates - -#### Decision (2026-09) - -`P.any(predicate)` takes a boolean predicate and a declared `T`. - -#### Why - -- Some checks are cheap as a boolean but awkward as a type guard (for example an - `every` over an array); `P.any` is the escape hatch. - -#### Known issue - -- As with `P.type`, the declared `T` is not proven by the predicate. Prefer - `P.when` whenever the check can be written as a guard. +Currently the library is placeholder code. diff --git a/development/publishing.md b/development/publishing.md index 70d8d6d..3f019b0 100644 --- a/development/publishing.md +++ b/development/publishing.md @@ -82,16 +82,12 @@ release commit. The version is derived from the graduated `## [x.y.z]` heading in [CHANGELOG.md](../CHANGELOG.md) and written to `package.json` + -`package-lock.json` by `npm version`. The README has no version line and does -not link `package.json`. +`package-lock.json` by `npm version`. #### Why - `release.sh` reads the version from the changelog, so the changelog is the input and `package.json` the derived copy — one direction, no drift. -- npm and Gitea render the version from package metadata, so a README copy would - be a third place to update for no reader benefit, and a link would only move - the lookup, not remove the copy. #### Rejected diff --git a/development/tooling.md b/development/tooling.md index 9d2b4c3..e5d7d5b 100644 --- a/development/tooling.md +++ b/development/tooling.md @@ -174,18 +174,16 @@ rules disabled in `.oxlintrc.json`. - The package is intentionally ESM-only (no CommonJS shim), so CJS resolution scenarios are out of scope by design, not a bug. -### `tslib` and `type-fest` are deliberately not used +### `tslib` is deliberately not used #### Decision (2026-09) -Neither `tslib` nor `type-fest` is a dependency. +`tslib` is not a dependency. #### Why - `tslib` is a runtime helper for old ES3/ES5 targets; this project targets ES2024. -- `type-fest` was never imported. -- `knip` flagged both, the same signal that keeps the list honest. ## Git hooks and script wiring