Files
tiny-pattern-ts/src/primitive.ts
T
tmu 800c11139d ♻️ Name the matcher factories getMatcher / getMatcherW
Drop the strict/widened aliases from src/index.ts so the factories are
exported under their own names, and align the type tests, the autocomplete
probe (which now imports the public surface from ./index.ts) and library.md
with them.

Also correct the widened-overload comment: the excess-property check does not
apply to a generic P, so MatcherWidening's keyof guard — not the closed
constraint — is what rejects keys outside T/_, as library.md already
documented.
2026-09-17 21:48:54 +00:00

64 lines
2.6 KiB
TypeScript

import type { ValueOf } from "type-fest";
type UnaryFn<T, R> = (shape: T) => R;
// `Extract` drops optional handlers (`undefined`) so `PatternReturns` also works
// when `P`'s constraint has optional keys.
type PatternReturns<P> = ReturnType<
Extract<ValueOf<P>, (...args: never[]) => unknown>
>;
type Handlers<R, T extends string | number> = { [K in T]: UnaryFn<K, R> };
// Fallback form: `_` is a required key, the T-keys are optional.
type Fallback<R, T extends string | number> = Partial<Handlers<R, T>> & {
_: UnaryFn<T, R>;
};
// Completion form: T-keys required, `_` optional. Overload #1, because
// TypeScript takes the *first* overload as the contextual type for the popup.
type ExhaustiveLoose<R, T extends string | number> = Handlers<R, T> & {
_?: UnaryFn<T, R>;
};
// oxlint-disable typescript/unified-signatures
// Strict returns: one common `R`. Overload order is load-bearing:
// #1 ExhaustiveLoose -> autocomplete `_?, a, b`
// #2 Fallback -> accepts a partial pattern
// #3 Handlers (last) -> "Property 'b' is missing" is the reported error
interface MatcherStrict<T extends string | number> {
<R>(pattern: ExhaustiveLoose<R, T>): UnaryFn<T, R>;
<R>(pattern: Fallback<R, T>): UnaryFn<T, R>;
<R>(pattern: Handlers<R, T>): UnaryFn<T, R>;
}
// oxlint-enable typescript/unified-signatures
// Widened returns: the union of every handler's return type. `P` is inferred
// from the whole parameter, whose closed constraint supplies the
// contextual/autocomplete type; the `keyof P` guard appended to each overload
// rejects keys outside `T`/`_`.
interface MatcherWidening<T extends string | number> {
<P extends ExhaustiveLoose<unknown, T>>(
pattern: P & (keyof P extends T | "_" ? unknown : never),
): UnaryFn<T, PatternReturns<P>>;
<P extends Fallback<unknown, T>>(
pattern: P & (keyof P extends T | "_" ? unknown : never),
): UnaryFn<T, PatternReturns<P>>;
<P extends Handlers<unknown, T>>(
pattern: P & (keyof P extends T | "_" ? unknown : never),
): UnaryFn<T, PatternReturns<P>>;
}
type HandlerMap = Record<string | number, UnaryFn<never, unknown> | undefined>;
const dispatch =
(pattern: HandlerMap) =>
(shape: string | number): unknown =>
// oxlint-disable-next-line typescript/no-non-null-assertion typescript/no-unsafe-type-assertion
(pattern[shape] ?? pattern["_"]!)(shape as never);
export const getMatcher = <T extends string | number>(): MatcherStrict<T> =>
dispatch;
export const getMatcherW = <T extends string | number>(): MatcherWidening<T> =>
dispatch;