✨ Add initial pattern-matching API

Introduce the core builder and pattern constructors:

- match(value): chain .with(pattern, handler) and terminate with
  .exhaustive() (throws on no match) or .otherwise(handler).
- P.literal, P.type, P.when, P.any, P.shape for pattern construction.
- Each pattern is a Matcher<T> whose matches acts as a type guard, so
  handler parameters are narrowed to the matched type.

The MatchBuilder accumulates handlers and returns a new builder per
.with() call (immutable chaining); finalization runs the cases in order
and returns the first match's handler result.

TypeScript configuration relaxed in .oxlintrc.json:
- Disable eslint/no-undefined (we use undefined as the no-match sentinel)
- Disable eslint/sort-keys (handler order matters; alphabetical would
  be wrong)
- Disable eslint/id-length (T/R/U/V generics are standard TS convention)
- Disable import/no-named-export (false positive for library entry points)

Verified:
- tsc --noEmit passes with strict + noUncheckedIndexedAccess
- tsc -p tsconfig.build.json emits dist/*.js + .d.ts + sourcemaps
- node -e "import('./dist/index.js')" loads and exposes { match, P }
This commit is contained in:
tmu committed 2026-09-03 10:35:34 +00:00
1 parent 6c67e91e68
commit 08384bbc82
4 files changed
+160 -2

No files matched your search

+2 -1
View File
@@ -1 +1,2 @@
export {};
export { match, P } from "./match.js";
export type { Matcher, Pattern } from "./pattern.js";
+63
View File
@@ -0,0 +1,63 @@
import { P, type Matcher, type Pattern } from "./pattern.js";
type Cases<R> = ReadonlyArray<
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 as unknown)) {
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,
[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 as unknown)) {
return run(value) as R;
}
}
return handler(value);
},
};
return builder;
};
export const match = <T>(value: T): MatchBuilder<T, never> =>
buildMatch<T, never>(value, []);
export type { Matcher, Pattern };
export { P };
+89
View File
@@ -0,0 +1,89 @@
/**
* 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 };
};
const whenMatcher = <T>(
predicate: (value: unknown) => value is T,
): Matcher<T> => ({
matches: predicate,
});
const whenMatcherAny = <T>(
predicate: (value: unknown) => boolean,
): Matcher<T> => ({
matches: (value: unknown): value is T => predicate(value),
});
const 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;
const candidate = value as S;
for (const key of Object.keys(shape) as Array<keyof S>) {
if (!(key in candidate)) return false;
const expected = shape[key];
const actual = candidate[key];
if (
typeof expected === "object" &&
expected !== null &&
"matches" in expected
) {
const nested = expected as unknown as Matcher<unknown>;
if (!nested.matches(actual)) return false;
} else if (actual !== expected) {
return false;
}
}
return refine
? refine(candidate)
: (true as T extends S ? true : never);
},
});
export const P = {
literal: literalMatcher,
type: typeMatcher,
when: whenMatcher,
any: whenMatcherAny,
shape: structuralMatcher,
} as const;
export type Pattern<T> = Matcher<T>;