test:ci now runs c8 with --all --include "src/**/*.ts" --100, so the build job fails when any runtime file under src/ is untested. --all is what makes the gate non-vacuous: without it c8 counts only the files the suite happened to load, and a new untested module stays invisible. Add src/index.test.ts to load the public barrel, which was previously never imported at runtime and so read as 0% under --all. matcher-shared.ts is types-only (an empty runtime image) and carries a file-level c8 ignore with the reason. Why 100% and the rejected alternatives: development/ci.md § Coverage threshold.
103 lines
4.5 KiB
TypeScript
103 lines
4.5 KiB
TypeScript
/* c8 ignore start -- types only: the module has no runtime image to cover */
|
|
import type { IsLiteral, IsNever, ValueOf } from "type-fest";
|
|
|
|
// The primitive-union and tagged-union matchers differ in their universe, but the
|
|
// handler/fallback plumbing is identical; these are the shared pieces. The
|
|
// boundary is deliberate: `Handlers`, `Fallback` and `MustBePartial` stay with
|
|
// each matcher because they are built from its universe. See development/library.md.
|
|
|
|
// A handler: one universe member in, one return value out.
|
|
export type UnaryFn<T, R> = (shape: T) => R;
|
|
|
|
// The primitive universe a matcher can discriminate. The primitive-union matcher
|
|
// uses it directly; the tagged-union matcher uses it as the set of allowed
|
|
// discriminant (`Tag`) values. `boolean` is admitted as the pair `true | false`;
|
|
// see README § Caveats for the unsupported members.
|
|
export type Matchable = string | number | boolean | null | undefined;
|
|
|
|
// `boolean`, `null` and `undefined` cannot be property keys, so a mapped type
|
|
// over a universe that includes one keys each such member by its
|
|
// stringification. `Member` inverts that projection against the universe, so a
|
|
// handler callback still receives the *real* member (`true`, not `"true"`).
|
|
// Both matchers use the projection: the primitive-union matcher over its
|
|
// universe, the tagged-union matcher over a discriminant property's values. See
|
|
// README § Caveats for the limits.
|
|
export type PatternKey<T> = T extends boolean
|
|
? T extends true
|
|
? "true"
|
|
: "false"
|
|
: T extends null
|
|
? "null"
|
|
: T extends undefined
|
|
? "undefined"
|
|
: T;
|
|
|
|
// The member(s) of `T` whose `PatternKey` is `K`: the universe-keyed inverse of
|
|
// `PatternKey`. The key alone cannot recover the member (`"true"` and `true`
|
|
// share it), so the handler parameter is derived from `T` instead. For a
|
|
// supported (injective) universe the result is a single member.
|
|
export type Member<
|
|
T extends Matchable,
|
|
K extends PropertyKey,
|
|
> = T extends Matchable ? (PatternKey<T> extends K ? T : never) : never;
|
|
|
|
// The property key a `Matchable` member takes at runtime: booleans, `null` and
|
|
// `undefined` stringify, and a numeric literal becomes its decimal string.
|
|
export type Stringified<T> = T extends boolean
|
|
? T extends true
|
|
? "true"
|
|
: "false"
|
|
: T extends null
|
|
? "null"
|
|
: T extends undefined
|
|
? "undefined"
|
|
: T extends number
|
|
? `${T}`
|
|
: never;
|
|
|
|
// The members of `T` that are also the stringification of another member, so
|
|
// `PatternKey` cannot invert them. `never` means the universe is injective.
|
|
export type Collisions<T> = Extract<T, Stringified<T>>;
|
|
|
|
// Why `T` is not a supported universe, or `never` when it is. A supported
|
|
// universe is a finite union of literals with no value/stringification
|
|
// collision: only then can `PatternKey` be inverted unambiguously.
|
|
export type UnsupportedReason<T extends Matchable> =
|
|
IsLiteral<PatternKey<T>> extends true
|
|
? [Collisions<T>] extends [never]
|
|
? never
|
|
: `a value and its stringification collide. Value is "${Collisions<T> & string}"`
|
|
: "broad types like string, number and template literals are not supported";
|
|
|
|
// The diagnostic for an unsupported universe. Extends `HandlerMap` so the
|
|
// implementation's `handlers: HandlerMap` stays assignable when the gate is
|
|
// intersected into a parameter; the property name is the message.
|
|
export type UnsupportedUniverse<Reason extends string> = HandlerMap &
|
|
Readonly<Record<`unsupported universe: ${Reason}`, never>>;
|
|
|
|
// `unknown` for a supported universe (an intersection no-op), the diagnostic
|
|
// otherwise. Intersecting rather than branching keeps `R` inference intact.
|
|
export type UniverseGate<T extends Matchable> =
|
|
IsNever<UnsupportedReason<T>> extends true
|
|
? unknown
|
|
: UnsupportedUniverse<UnsupportedReason<T>>;
|
|
|
|
// `Extract` drops optional handlers (`undefined`) so `PatternReturns` also works
|
|
// when `P`'s constraint has optional keys.
|
|
export type PatternReturns<P> = ReturnType<
|
|
Extract<ValueOf<P>, (...args: never[]) => unknown>
|
|
>;
|
|
|
|
// The diagnostic raised when a fallback is supplied for an already-exhaustive
|
|
// handler map. Each matcher's `MustBePartial` folds it into `Handled`'s
|
|
// constraint so the guard is checked after inference.
|
|
export interface RedundantFallback {
|
|
readonly "every case is already handled, so the fallback is redundant": never;
|
|
}
|
|
|
|
// The runtime dispatch map the handler maps and fallback erase to.
|
|
export type HandlerMap = Record<
|
|
string | number,
|
|
UnaryFn<never, unknown> | undefined
|
|
>;
|