/** * Fluent, callable predicate combinators that preserve TypeScript type guards. * * A Predicate is: * - Callable as `(value: T) => value is U` * - Composable through `.and()`, `.or()`, and `.negate()` * * Ordinary boolean predicates are treated as narrowing to `T`, meaning they * do not narrow the input type by themselves. Composed predicates passed to * `.and()` / `.or()` may return any value; they are tested for truthiness. * * @module */ /** An ordinary boolean predicate. */ export type PredicateFunction = (value: T) => boolean; /** A predicate that narrows T to U. */ export type TypePredicateFunction = (value: T) => value is U; /** A predicate tested for truthiness when composed with `.and()` / `.or()`. */ export type PredicateInput = (value: T) => unknown; /** * Extracts the narrowed type from a type predicate. * * Ordinary boolean predicates do not narrow, so they produce T. A guard whose * narrowed type is disjoint from T (`Extract` is `never`) is treated as a * non-narrowing filter and keeps T, rather than collapsing the chain to `never` - * this is what a negated narrowing guard looks like (e.g. `hasName(...).negate()`, * which widens back to a supertype of T). */ type NarrowedBy = P extends (value: any) => value is infer U ? [Extract] extends [never] ? T : Extract : T; /** Intersects the narrowed types produced by a tuple of predicates. */ type AndNarrowed[], Result = T> = P extends readonly [ infer First, ...infer Rest extends readonly PredicateInput[], ] ? AndNarrowed> : Result; /** Unions the narrowed types produced by a tuple of predicates. */ type OrNarrowed[], Result = never> = P extends readonly [ infer First, ...infer Rest extends readonly PredicateInput[], ] ? OrNarrowed> : Result; /** * A callable predicate with fluent composition methods. * * T is the accepted input type. * U is the type established when the predicate returns true. */ export interface Predicate { (value: T): value is U; /** * Returns a predicate requiring this predicate and every supplied predicate * to match. * * Type guards are intersected. Additional predicates are checked against the * type already established by this predicate. */ and[]>( ...predicates: P ): Predicate, T>>; /** * Returns a predicate requiring this predicate or any supplied predicate * to match. * * Type guards are unioned. Because an ordinary boolean predicate could * accept any value of `U`, including one causes the resulting predicate to * narrow only to `U`. */ or[]>( ...predicates: P ): Predicate, T>>; /** * Returns the logical inverse of this predicate. * * For a type predicate narrowing T to U, the result narrows to * Exclude. */ negate(): Predicate>; } /** Coerce a predicate result to boolean the way `if (...)` does. */ function isTruthy(test: (value: T) => unknown): PredicateFunction { return (value) => Boolean(test(value)); } /** * Creates the callable predicate object. * * The public generic behavior is provided by Predicate. Runtime * composition coerces predicate results to boolean; type predicates are * ordinary boolean functions at runtime. */ function buildPredicate(test: (value: T) => unknown): Predicate { const check = isTruthy(test); const callable = ((value: T): value is U => check(value)) as (value: T) => value is U; return Object.assign(callable, { and[]>( ...predicates: P ): Predicate, T>> { type Result = Extract, T>; return buildPredicate( (value) => check(value) && predicates.every((predicate) => isTruthy(predicate)(value as U)), ); }, or[]>( ...predicates: P ): Predicate, T>> { type Result = Extract, T>; return buildPredicate( (value) => check(value) || predicates.some((predicate) => isTruthy(predicate)(value as U)), ); }, negate(): Predicate> { return buildPredicate>((value) => !check(value)); }, }) as Predicate; } /** * Wraps a type predicate while preserving its narrowed type. */ export function create(predicate: TypePredicateFunction): Predicate; /** * Wraps an ordinary boolean or truthy predicate. */ export function create(predicate: PredicateInput): Predicate; export function create( predicate: TypePredicateFunction | PredicateInput, ): Predicate { return buildPredicate(predicate); }