🔀 Merge chore/cleanup-readme into main
This commit is contained in:
commit
09011d838f
5 files changed
+148
-13
No files matched your search
@@ -7,7 +7,7 @@ first-action facts. Do not restate evolving prose here — it will drift.
|
|||||||
|
|
||||||
## First action
|
## First action
|
||||||
|
|
||||||
- Project: F#-style pattern matching for TypeScript/ESM. Node `>=26` (pinned via `.node-version`), ESM-only (no CommonJS shim).
|
- Project: pattern matching for TypeScript/ESM. Node `>=26` (pinned via `.node-version`), ESM-only (no CommonJS shim).
|
||||||
- **While iterating:** `npm run test` (`check:tsc` + the unit suite) for fast feedback on the files you changed.
|
- **While iterating:** `npm run test` (`check:tsc` + the unit suite) for fast feedback on the files you changed.
|
||||||
- **Optional code intelligence:** this repo installs `@spences10/pi-lsp` (pinned in `.pi/settings.json`) as a project-local pi extension. It talks to the repo's own TypeScript 7 via `tsc --lsp --stdio` and exposes **read-only** tools — `lsp_hover`, `lsp_definition`, `lsp_references`, `lsp_find_symbol`, `lsp_document_symbols`, `lsp_diagnostics(_many)`. **When you are looking for a symbol, reach for the LSP before `rg`/`grep`** — `lsp_references` / `lsp_find_symbol` / `lsp_definition` / `lsp_document_symbols` are semantic and cross-file, so they see shadowing, imports and overloads that a text search cannot; use `lsp_hover` to read inferred types on generic-heavy code. Use `rg` for what the LSP cannot see — doc prose, string literals, config, task lists, file discovery — and reconcile the two sets before editing (symbols from the LSP, strings and prose from `rg`). It has no rename / code-action / apply-edit surface — the write side is pi's `edit` tool + `check:tsc`. Treat empty LSP output as _inconclusive_, not success: **`npm run test` / `npm run verify` remain the sole authoritative gate** (see the next bullet). The server keeps running across that gate with a ~5 min idle timeout and registers no file watchers, so if you change `tsconfig.json` / `package.json` mid-session its diagnostics can be stale — when LSP output disagrees with `check:tsc`, trust `check:tsc` and restart pi (or wait out the idle timeout) before concluding the LSP is wrong.
|
- **Optional code intelligence:** this repo installs `@spences10/pi-lsp` (pinned in `.pi/settings.json`) as a project-local pi extension. It talks to the repo's own TypeScript 7 via `tsc --lsp --stdio` and exposes **read-only** tools — `lsp_hover`, `lsp_definition`, `lsp_references`, `lsp_find_symbol`, `lsp_document_symbols`, `lsp_diagnostics(_many)`. **When you are looking for a symbol, reach for the LSP before `rg`/`grep`** — `lsp_references` / `lsp_find_symbol` / `lsp_definition` / `lsp_document_symbols` are semantic and cross-file, so they see shadowing, imports and overloads that a text search cannot; use `lsp_hover` to read inferred types on generic-heavy code. Use `rg` for what the LSP cannot see — doc prose, string literals, config, task lists, file discovery — and reconcile the two sets before editing (symbols from the LSP, strings and prose from `rg`). It has no rename / code-action / apply-edit surface — the write side is pi's `edit` tool + `check:tsc`. Treat empty LSP output as _inconclusive_, not success: **`npm run test` / `npm run verify` remain the sole authoritative gate** (see the next bullet). The server keeps running across that gate with a ~5 min idle timeout and registers no file watchers, so if you change `tsconfig.json` / `package.json` mid-session its diagnostics can be stale — when LSP output disagrees with `check:tsc`, trust `check:tsc` and restart pi (or wait out the idle timeout) before concluding the LSP is wrong.
|
||||||
- **Definition of done — run this before you call the work finished:** `npm run verify`. If all green, commit. If red, look at the output, fix the root cause, and re-run.
|
- **Definition of done — run this before you call the work finished:** `npm run verify`. If all green, commit. If red, look at the output, fix the root cause, and re-run.
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- restore the README's Synopsis and Examples sections: a quick-start recipe plus
|
||||||
|
real-world examples (primitive-union dispatch, a fallback, a property union
|
||||||
|
narrowed by the tagged-union matcher, and a widening variant)
|
||||||
- document the public API in the README: the four factories, when to use each,
|
- 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
|
what the `W` (widening) suffix means, and examples that bind the handler-map
|
||||||
function once
|
function once
|
||||||
|
|||||||
@@ -1,15 +1,53 @@
|
|||||||
# tiny-pattern-ts
|
# tiny-pattern-ts
|
||||||
|
|
||||||
Pattern matching for TypeScript/ESM environments (F#-style, not regex).
|
Exhaustive, type-safe pattern matching for TypeScript.
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getTaggedUnionMatcher } from "tiny-pattern-ts";
|
||||||
|
|
||||||
|
// 1. We have a union type
|
||||||
|
type Contact =
|
||||||
|
| { kind: "email"; address: string }
|
||||||
|
| { kind: "phone"; number: string }
|
||||||
|
| { kind: "messenger"; username: string };
|
||||||
|
|
||||||
|
// 2. Create a matcher providing the discriminant property
|
||||||
|
const matchContact = getTaggedUnionMatcher<Contact>()("kind");
|
||||||
|
|
||||||
|
// 3. Define handlers for each branch of the union
|
||||||
|
const formatContact = matchContact({
|
||||||
|
email: (e) => `MAIL: ${e.address}`,
|
||||||
|
phone: (p) => `PHONE: ${p.number}`,
|
||||||
|
messenger: (m) => `MESSENGER: @${m.username}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Call the matcher with a value
|
||||||
|
const mailOutput = formatContact({ kind: "email", address: "ada@example.com" });
|
||||||
|
assert.equal(mailOutput, "MAIL: ada@example.com");
|
||||||
|
|
||||||
|
const phoneOutput = formatContact({ kind: "phone", number: "+1 555 0100" });
|
||||||
|
assert.equal(phoneOutput, "PHONE: +1 555 0100");
|
||||||
|
```
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
`tiny-pattern-ts` brings F#-style pattern matching to TypeScript. Patterns are
|
`tiny-pattern-ts` is a pattern-matching library for TypeScript.
|
||||||
ordinary objects whose `matches` method is a TypeScript type guard, so narrowing
|
|
||||||
composes the way any other guard does. It is deliberately not a regex engine and
|
The main goal of `tiny-pattern-ts` is to make pattern matching type-safe with a
|
||||||
not a macro: there is no transpiler and no DSL to learn, and the type-level
|
lean syntax. This is accomplished by being exhaustive and passing typed
|
||||||
contract is the feature — see [development/library.md](./development/library.md)
|
parameters per branch to the handlers — supported by an outstanding
|
||||||
for the design decisions and [Caveats](#caveats) for the limits.
|
autocomplete and a tiny footprint.
|
||||||
|
|
||||||
|
See [development/library.md](./development/library.md) for the design decisions
|
||||||
|
and [Caveats](#caveats) for the limits.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install tiny-pattern-ts
|
||||||
|
```
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -19,6 +57,100 @@ for the design decisions and [Caveats](#caveats) for the limits.
|
|||||||
both resolve on TS >= 5.0 in `node10` / `node16` / `nodenext` / `bundler`.
|
both resolve on TS >= 5.0 in `node10` / `node16` / `nodenext` / `bundler`.
|
||||||
- The package is **ESM-only** (no CommonJS shim).
|
- The package is **ESM-only** (no CommonJS shim).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
A few real-world recipes. Each binds the handler-map function once and reuses
|
||||||
|
it, so the matcher is allocated a single time.
|
||||||
|
|
||||||
|
### Dispatch on a primitive union
|
||||||
|
|
||||||
|
A result code is itself a finite union, so `getPrimitiveUnionMatcher` keys a
|
||||||
|
handler on each member:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getPrimitiveUnionMatcher } from "tiny-pattern-ts";
|
||||||
|
|
||||||
|
type ResultCode = "ok" | "created" | "no-content";
|
||||||
|
|
||||||
|
const toStatus = getPrimitiveUnionMatcher<ResultCode>()({
|
||||||
|
ok: () => 200,
|
||||||
|
created: () => 201,
|
||||||
|
"no-content": () => 204,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(toStatus("ok"), 200);
|
||||||
|
assert.equal(toStatus("created"), 201);
|
||||||
|
assert.equal(toStatus("no-content"), 204);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Leave cases to a fallback
|
||||||
|
|
||||||
|
Pass a fallback as the second argument to handle only part of the universe; it
|
||||||
|
receives the members the map leaves uncovered — here the parameter is
|
||||||
|
`"deprecated" | "gateway-timeout"`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getPrimitiveUnionMatcher } from "tiny-pattern-ts";
|
||||||
|
|
||||||
|
type Status = "active" | "beta" | "deprecated" | "gateway-timeout";
|
||||||
|
|
||||||
|
const rollout = getPrimitiveUnionMatcher<Status>()(
|
||||||
|
{ active: () => "enabled", beta: () => "enabled" },
|
||||||
|
(status) => `blocked (${status})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(rollout("active"), "enabled");
|
||||||
|
assert.equal(rollout("deprecated"), "blocked (deprecated)");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dispatch on a property union
|
||||||
|
|
||||||
|
The value does not have to be the union itself. When a single property carries a
|
||||||
|
finite union, the tagged-union matcher keys on it and narrows the whole record to
|
||||||
|
the selected value:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getTaggedUnionMatcher } from "tiny-pattern-ts";
|
||||||
|
|
||||||
|
interface Invoice {
|
||||||
|
readonly currency: "eur" | "usd" | "jpy";
|
||||||
|
readonly amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchCurrency = getTaggedUnionMatcher<Invoice>()("currency");
|
||||||
|
|
||||||
|
const symbolOf = matchCurrency({
|
||||||
|
eur: (i) => `€${i.amount.toFixed(2)}`,
|
||||||
|
usd: (i) => `$${i.amount.toFixed(2)}`,
|
||||||
|
jpy: (i) => `¥${i.amount.toFixed(0)}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(symbolOf({ currency: "usd", amount: 12.5 }), "$12.50");
|
||||||
|
assert.equal(symbolOf({ currency: "jpy", amount: 900 }), "¥900");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Widen the return type
|
||||||
|
|
||||||
|
When the handlers return different types, reach for the widening `W` variant: the
|
||||||
|
matcher's return is their union rather than one common type — here
|
||||||
|
`string | string[] | undefined`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getPrimitiveUnionMatcherW } from "tiny-pattern-ts";
|
||||||
|
|
||||||
|
type Field = "name" | "tags" | "note";
|
||||||
|
|
||||||
|
const parse = getPrimitiveUnionMatcherW<Field>()({
|
||||||
|
name: () => "Ada",
|
||||||
|
tags: () => ["admin", "beta"],
|
||||||
|
note: () => undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(parse("name"), "Ada");
|
||||||
|
assert.deepEqual(parse("tags"), ["admin", "beta"]);
|
||||||
|
assert.equal(parse("note"), undefined);
|
||||||
|
```
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
The package exports four factories. Two axes pick one:
|
The package exports four factories. Two axes pick one:
|
||||||
@@ -38,7 +170,7 @@ The package exports four factories. Two axes pick one:
|
|||||||
| `getTaggedUnionMatcherW<T>()` | the value is a discriminated object and handlers return different types | union of the handlers |
|
| `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
|
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.
|
reuse it; the [Examples](#examples) do this, so the builder is allocated once.
|
||||||
|
|
||||||
### Primitive-union matchers
|
### Primitive-union matchers
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -23,15 +23,15 @@ Matcher:
|
|||||||
✔ optional discriminant (`{ type?: "x" }`) is the same hole: the boolean/nullish change now admits the `undefined` tag, so the factory accepts the key, but `Extract<T, Record<K, V>>` still passes `never` to both the `x` and `undefined` handlers @done
|
✔ optional discriminant (`{ type?: "x" }`) is the same hole: the boolean/nullish change now admits the `undefined` tag, so the factory accepts the key, but `Extract<T, Record<K, V>>` still passes `never` to both the `x` and `undefined` handlers @done
|
||||||
|
|
||||||
Documentation:
|
Documentation:
|
||||||
☐ Bring README.md back to its previous form — synopsis and examples restored, in the correct place
|
✔ Bring README.md back to its previous form — synopsis and examples restored, in the correct place @done
|
||||||
→ previous section order: title, tagline, Synopsis, Description, Requirements, Examples, API, License, Contributing
|
→ previous section order: title, tagline, Synopsis, Description, Requirements, Examples, API, License, Contributing
|
||||||
→ 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 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
|
☐ 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 @done (9/24/2026, 10:21:17 PM)
|
||||||
☐ Create backlog tasks for implementation
|
✔ Create backlog tasks for implementation @done (9/24/2026, 10:21:16 PM)
|
||||||
✔ Validate code fences in Markdown (start with README.md) — compile the TypeScript examples against `src/` so the docs cannot drift from the API @done
|
✔ Validate code fences in Markdown (start with README.md) — compile the TypeScript examples against `src/` so the docs cannot drift from the API @done
|
||||||
|
|
||||||
Maintenance:
|
Maintenance:
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "tiny-pattern-ts",
|
"name": "tiny-pattern-ts",
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"description": "Pattern matching for TypeScript/ESM environments (F#-style, not regex)",
|
"description": "Exhaustive, type-safe pattern matching for TypeScript",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"adt",
|
"adt",
|
||||||
"algebraic-data-types",
|
"algebraic-data-types",
|
||||||
|
|||||||
Reference in new issue
Block a user