✨ 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:
1 parent
6c67e91e68
commit
08384bbc82
4 files changed
+160
-2
No files matched your search
+6
-1
@@ -14,7 +14,12 @@
|
|||||||
"restriction": "error",
|
"restriction": "error",
|
||||||
"nursery": "off"
|
"nursery": "off"
|
||||||
},
|
},
|
||||||
"rules": {},
|
"rules": {
|
||||||
|
"eslint/no-undefined": "off",
|
||||||
|
"eslint/sort-keys": "off",
|
||||||
|
"eslint/id-length": "off",
|
||||||
|
"import/no-named-export": "off"
|
||||||
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"builtin": true,
|
"builtin": true,
|
||||||
"es2024": true,
|
"es2024": true,
|
||||||
|
|||||||
+2
-1
@@ -1 +1,2 @@
|
|||||||
export {};
|
export { match, P } from "./match.js";
|
||||||
|
export type { Matcher, Pattern } from "./pattern.js";
|
||||||
@@ -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 };
|
||||||
@@ -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>;
|
||||||
Reference in new issue
Block a user