/** * Pattern matching primitives. Each constructor returns a lightweight * matcher object whose `matches` method returns a type guard. */ export interface Matcher { readonly matches: (value: unknown) => value is T; } const literalMatcher = < const L extends string | number | boolean | null | undefined, >( value: L, ): Matcher => ({ matches: (candidate): candidate is L => candidate === value, }); const typeMatcher = ( type: | "string" | "number" | "boolean" | "bigint" | "symbol" | "undefined" | "object" | "function", ): Matcher => { 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 = ( predicate: (value: unknown) => value is T, ): Matcher => ({ matches: predicate, }), whenMatcherAny = ( predicate: (value: unknown) => boolean, ): Matcher => ({ matches: (value: unknown): value is T => predicate(value), }), isNestedMatcher = (expected: unknown): expected is Matcher => typeof expected === "object" && expected !== null && "matches" in expected, // oxlint-disable-next-line typescript/no-unnecessary-type-parameters keysMatch = (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 = ( shape: S, refine?: (value: S) => value is T, ): Matcher => ({ 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 = Matcher;