🔥 Remove the match and pattern example modules
They were the template's demo API, not the library's real surface. Drop them, their README walkthrough, and the tooling note that cited their disable directives, and point index.ts at the primitive matchers that remain.
This commit is contained in:
1 parent
ce3d618757
commit
a3eb6183af
5 files changed
+14
-315
No files matched your search
@@ -2,34 +2,14 @@
|
|||||||
|
|
||||||
Pattern matching for TypeScript/ESM environments (F#-style, not regex).
|
Pattern matching for TypeScript/ESM environments (F#-style, not regex).
|
||||||
|
|
||||||
## Synopsis
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { match, P } from "tiny-pattern-ts";
|
|
||||||
|
|
||||||
const reply = (answer: "yes" | "no") =>
|
|
||||||
match(answer)
|
|
||||||
.with(P.literal("yes"), (): "agreed" => "agreed")
|
|
||||||
.with(P.literal("no"), (): "declined" => "declined")
|
|
||||||
.exhaustive();
|
|
||||||
|
|
||||||
reply("yes"); // "agreed"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
`tiny-pattern-ts` gives TypeScript the shape of F#-style pattern matching:
|
`tiny-pattern-ts` brings F#-style pattern matching to TypeScript. Patterns are
|
||||||
a value flows through a chain of patterns, the first one that matches runs its
|
ordinary objects whose `matches` method is a TypeScript type guard, so narrowing
|
||||||
handler, and the handler receives the value narrowed to that pattern's type. The
|
composes the way any other guard does. It is deliberately not a regex engine and
|
||||||
"patterns" are ordinary objects whose `matches` method is a TypeScript type
|
not a macro: there is no transpiler and no DSL to learn, and the type-level
|
||||||
guard, so narrowing composes the way any other guard does.
|
contract is the feature — see [development/library.md](./development/library.md)
|
||||||
|
for the design decisions and the known limitations.
|
||||||
It is deliberately not a regex engine and not a macro. There is no transpiler
|
|
||||||
and no DSL to learn: `match(value)` returns a builder, `.with(pattern, handler)`
|
|
||||||
adds a case, and the chain ends in either `.exhaustive()` or `.otherwise(...)`.
|
|
||||||
The type-level contract is the feature — see
|
|
||||||
[development/library.md](./development/library.md) for the design decisions and
|
|
||||||
the known limitations.
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -39,124 +19,6 @@ the known limitations.
|
|||||||
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
|
|
||||||
|
|
||||||
### Literal matching and `exhaustive()`
|
|
||||||
|
|
||||||
`.exhaustive()` returns the union of the handler return types and throws if no
|
|
||||||
case matched. Annotate handler returns when you want literal types rather than
|
|
||||||
`string`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type Answer = "yes" | "no";
|
|
||||||
|
|
||||||
const reply = (answer: Answer): "agreed" | "declined" =>
|
|
||||||
match(answer)
|
|
||||||
.with(P.literal("yes"), (): "agreed" => "agreed")
|
|
||||||
.with(P.literal("no"), (): "declined" => "declined")
|
|
||||||
.exhaustive();
|
|
||||||
|
|
||||||
reply("yes"); // "agreed"
|
|
||||||
```
|
|
||||||
|
|
||||||
`exhaustive()` checks at runtime, not at compile time — TypeScript does not force
|
|
||||||
every union member to have a case (see
|
|
||||||
[development/library.md](./development/library.md#exhaustive-is-a-runtime-check)).
|
|
||||||
Use `.otherwise(...)` when a fallback is wanted:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const label = (answer: Answer): string =>
|
|
||||||
match(answer)
|
|
||||||
.with(P.literal("yes"), () => "agreed")
|
|
||||||
.otherwise(() => "not agreed");
|
|
||||||
```
|
|
||||||
|
|
||||||
### Matching by `typeof`
|
|
||||||
|
|
||||||
`P.type<T>(name)` pairs an explicit type `T` with the runtime `typeof` name it
|
|
||||||
should test for:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const describe = (value: unknown): string =>
|
|
||||||
match(value)
|
|
||||||
.with(P.type<string>("string"), (s) => `string of length ${s.length}`)
|
|
||||||
.with(P.type<number>("number"), (n) => `number ${n.toFixed(2)}`)
|
|
||||||
.otherwise(() => "something else");
|
|
||||||
```
|
|
||||||
|
|
||||||
The supported names are `string`, `number`, `boolean`, `bigint`, `symbol`,
|
|
||||||
`undefined`, `object`, and `function`. `"object"` matches non-null objects and
|
|
||||||
functions; `"undefined"` compares against `undefined` directly.
|
|
||||||
|
|
||||||
### Structural matching and discriminated unions
|
|
||||||
|
|
||||||
`P.shape(shape, refine?)` checks that every key in `shape` exists on the value.
|
|
||||||
A value that is itself a matcher is applied, otherwise it is compared with
|
|
||||||
strict equality. To narrow to a concrete type, pass a `refine` type guard:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
interface Circle {
|
|
||||||
readonly kind: "circle";
|
|
||||||
readonly radius: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Square {
|
|
||||||
readonly kind: "square";
|
|
||||||
readonly side: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Shape = Circle | Square;
|
|
||||||
|
|
||||||
const area = (shape: Shape): number =>
|
|
||||||
match(shape)
|
|
||||||
.with(
|
|
||||||
P.shape({ kind: "circle" }, (v): v is Circle => "radius" in v),
|
|
||||||
(c) => Math.PI * c.radius ** 2,
|
|
||||||
)
|
|
||||||
.with(
|
|
||||||
P.shape({ kind: "square" }, (v): v is Square => "side" in v),
|
|
||||||
(s) => s.side ** 2,
|
|
||||||
)
|
|
||||||
.exhaustive();
|
|
||||||
```
|
|
||||||
|
|
||||||
Without `refine`, `P.shape` returns a matcher for the shape's own type, not the
|
|
||||||
narrowed one. Nested matchers can be used in the shape object, for example
|
|
||||||
`P.shape({ name: P.type<string>("string") })`.
|
|
||||||
|
|
||||||
### Custom guards with `when`
|
|
||||||
|
|
||||||
`P.when` takes a type guard and infers the narrowed type from it:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const toNumber = (value: unknown): number =>
|
|
||||||
match(value)
|
|
||||||
.with(
|
|
||||||
P.when((v): v is string => typeof v === "string"),
|
|
||||||
(s) => Number.parseInt(s, 10),
|
|
||||||
)
|
|
||||||
.otherwise(() => 0);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Widening with `any`
|
|
||||||
|
|
||||||
`P.any<T>(predicate)` takes a plain boolean predicate and a declared type `T`,
|
|
||||||
for cases where the predicate cannot be written as a type guard:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const firstNumber = (items: readonly unknown[]): number | undefined =>
|
|
||||||
match(items)
|
|
||||||
.with(
|
|
||||||
P.any<readonly number[]>(
|
|
||||||
(v) =>
|
|
||||||
Array.isArray(v) &&
|
|
||||||
v.every((item) => typeof item === "number"),
|
|
||||||
),
|
|
||||||
(xs) => xs[0],
|
|
||||||
)
|
|
||||||
.otherwise(() => undefined);
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
Yet to be implemented
|
Yet to be implemented
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ Type-aware oxlint is enabled via `options.typeAware: true` in `.oxlintrc.json`
|
|||||||
#### Decision (2026-09)
|
#### Decision (2026-09)
|
||||||
|
|
||||||
A type-aware rule that false-positives is silenced with a source-level
|
A type-aware rule that false-positives is silenced with a source-level
|
||||||
`oxlint-disable` directive (see `src/pattern.ts`, `src/match.ts`,
|
`oxlint-disable` directive (see `src/primitive.ts`,
|
||||||
`src/index.test.ts`), not by turning the rule off in `.oxlintrc.json`.
|
`src/primitive.test.ts`), not by turning the rule off in `.oxlintrc.json`.
|
||||||
|
|
||||||
#### Why
|
#### Why
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -1,2 +1,6 @@
|
|||||||
export { match, P } from "./match.ts";
|
export {
|
||||||
export type { Matcher, Pattern } from "./pattern.ts";
|
getPrimitiveUnionMatcher,
|
||||||
|
getPrimitiveUnionMatcherPartial,
|
||||||
|
getPrimitiveUnionMatcherPartialW,
|
||||||
|
getPrimitiveUnionMatcherW,
|
||||||
|
} from "./primitive.ts";
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { P, type Matcher, type Pattern } from "./pattern.ts";
|
|
||||||
|
|
||||||
type Cases<R> = readonly (readonly [Matcher<unknown>, (value: unknown) => R])[];
|
|
||||||
|
|
||||||
interface MatchBuilder<T, R> {
|
|
||||||
with<U extends T, V>(
|
|
||||||
pattern: Matcher<U>,
|
|
||||||
handler: (value: U) => V,
|
|
||||||
): MatchBuilder<T, R | V>;
|
|
||||||
exhaustive(): R;
|
|
||||||
otherwise(handler: (value: T) => R): R;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildMatch = <T, R>(value: T, cases: Cases<R>): MatchBuilder<T, R> => {
|
|
||||||
const apply = (): R | undefined => {
|
|
||||||
for (const [matcher, handler] of cases) {
|
|
||||||
if (matcher.matches(value)) {
|
|
||||||
return handler(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const builder = {
|
|
||||||
with<U extends T, V>(
|
|
||||||
pattern: Matcher<U>,
|
|
||||||
handler: (value: U) => V,
|
|
||||||
): MatchBuilder<T, R | V> {
|
|
||||||
const nextCases: Cases<R | V> = [
|
|
||||||
...cases,
|
|
||||||
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
||||||
[pattern, handler as (value: unknown) => R | V],
|
|
||||||
];
|
|
||||||
return buildMatch(value, nextCases);
|
|
||||||
},
|
|
||||||
exhaustive(): R {
|
|
||||||
const result = apply();
|
|
||||||
if (result === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
"tiny-pattern-ts: match.exhaustive() called with no matching case",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
otherwise(handler: (value: T) => R): R {
|
|
||||||
for (const [matcher, run] of cases) {
|
|
||||||
if (matcher.matches(value)) {
|
|
||||||
return run(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return handler(value);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return builder;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const match = <T>(value: T): MatchBuilder<T, never> =>
|
|
||||||
buildMatch<T, never>(value, []);
|
|
||||||
|
|
||||||
export type { Matcher, Pattern };
|
|
||||||
export { P };
|
|
||||||
-105
@@ -1,105 +0,0 @@
|
|||||||
/**
|
|
||||||
* Pattern matching primitives. Each constructor returns a lightweight
|
|
||||||
* matcher object whose `matches` method returns a type guard.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface Matcher<T> {
|
|
||||||
readonly matches: (value: unknown) => value is T;
|
|
||||||
}
|
|
||||||
|
|
||||||
const literalMatcher = <
|
|
||||||
const L extends string | number | boolean | null | undefined,
|
|
||||||
>(
|
|
||||||
value: L,
|
|
||||||
): Matcher<L> => ({
|
|
||||||
matches: (candidate): candidate is L => candidate === value,
|
|
||||||
});
|
|
||||||
const typeMatcher = <T>(
|
|
||||||
type:
|
|
||||||
| "string"
|
|
||||||
| "number"
|
|
||||||
| "boolean"
|
|
||||||
| "bigint"
|
|
||||||
| "symbol"
|
|
||||||
| "undefined"
|
|
||||||
| "object"
|
|
||||||
| "function",
|
|
||||||
): Matcher<T> => {
|
|
||||||
const matches = (value: unknown): value is T => {
|
|
||||||
if (type === "undefined") {
|
|
||||||
return value === undefined;
|
|
||||||
}
|
|
||||||
if (type === "object") {
|
|
||||||
return (
|
|
||||||
(typeof value === "object" && value !== null) ||
|
|
||||||
typeof value === "function"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return typeof value === type;
|
|
||||||
};
|
|
||||||
return { matches };
|
|
||||||
},
|
|
||||||
whenMatcher = <T>(
|
|
||||||
predicate: (value: unknown) => value is T,
|
|
||||||
): Matcher<T> => ({
|
|
||||||
matches: predicate,
|
|
||||||
}),
|
|
||||||
whenMatcherAny = <T>(
|
|
||||||
predicate: (value: unknown) => boolean,
|
|
||||||
): Matcher<T> => ({
|
|
||||||
matches: (value: unknown): value is T => predicate(value),
|
|
||||||
}),
|
|
||||||
isNestedMatcher = (expected: unknown): expected is Matcher<unknown> =>
|
|
||||||
typeof expected === "object" &&
|
|
||||||
expected !== null &&
|
|
||||||
"matches" in expected,
|
|
||||||
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters
|
|
||||||
keysMatch = <S extends object>(shape: S, candidate: object): boolean => {
|
|
||||||
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
||||||
for (const key of Object.keys(shape) as (keyof S)[]) {
|
|
||||||
if (!(key in candidate)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
||||||
const expected = shape[key],
|
|
||||||
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
||||||
actual = candidate[key as keyof object];
|
|
||||||
if (isNestedMatcher(expected)) {
|
|
||||||
if (!expected.matches(actual)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (actual !== expected) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
structuralMatcher = <S extends object, T extends S>(
|
|
||||||
shape: S,
|
|
||||||
refine?: (value: S) => value is T,
|
|
||||||
): Matcher<T> => ({
|
|
||||||
matches: (value: unknown): value is T => {
|
|
||||||
if (typeof value !== "object" || value === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
||||||
const candidate = value as S;
|
|
||||||
if (!keysMatch(shape, candidate)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (refine && !refine(candidate)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const P = {
|
|
||||||
literal: literalMatcher,
|
|
||||||
type: typeMatcher,
|
|
||||||
when: whenMatcher,
|
|
||||||
any: whenMatcherAny,
|
|
||||||
shape: structuralMatcher,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type Pattern<T> = Matcher<T>;
|
|
||||||
Reference in new issue
Block a user