Cleaner separation of concerns:
- options.typeAware: true in .oxlintrc.json activates type-aware
rules declaratively (equivalent to --type-aware CLI flag, but
the script command stays clean: just 'oxlint ...')
- Remove the 3 type-aware rule disables from .oxlintrc.json
- Add source-level oxlint-disable directives instead:
- 4x typescript/no-unsafe-type-assertion (pattern.ts: keysMatch
Object.keys() cast, candidate[] cast; structuralMatcher value
as S cast; match.ts: handler as ... cast in nextCases)
- 1x typescript/no-unnecessary-type-parameters (pattern.ts:
keysMatch <S extends object>)
- 1x file-level typescript/no-floating-promises in index.test.ts
(expectTypeOf() is a sync type-assertion library that the
type-aware linter misidentifies)
Disabling rules at the source (next to the line that needs the
exemption) documents intent more clearly than a global config
override, and makes the trade-off visible to anyone reading the
code. Re-enabling a rule in the future only requires removing the
inline comment, not editing a central config.
106 lines
3.3 KiB
TypeScript
106 lines
3.3 KiB
TypeScript
/**
|
|
* 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>;
|