// https://stackoverflow.com/a/50375286/1486679 type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( k: infer I ) => void ? I : never; type InstanceOf = T extends { new (...args: any[]): infer U } ? U : never; const isObject = (x: unknown): x is { [key: string]: unknown } => typeof x === "object" && x !== null; /** Validator for type T. */ export type Validator = (x: unknown) => x is T; /** Type for which a validator matches. */ export type TypeOf = T extends Validator ? U : never; // -------------------------------- // // - - - PRIMITIVE VALIDATORS - - - // // -------------------------------- // /** Returns true if and only if x is a boolean. */ export const boolean = (x: unknown): x is boolean => typeof x === "boolean"; /** Returns true if and only if x is a string. */ export const string = (x: unknown): x is string => typeof x === "string"; /** Returns true if and only if x is a number. */ export const number = (x: unknown): x is number => typeof x === "number"; /** Returns true if and only if x is undefined. */ export const empty = (x: unknown): x is undefined => x === undefined; /** Returns true if and only if x is null. */ export const nil = (x: unknown): x is null => x === null; /** Returns true if and only if x is strictly equal to y. */ export const literal = (y: T) => (x: unknown): x is T => x === y; // ------------------------------ // // - - - COMPLEX VALIDATORS - - - // // ------------------------------ // /** Returns true if and only if x is an object where each value matches the given validator. */ export const object = (validator: T) => (x: unknown): x is { [key: string]: TypeOf } => isObject(x) && Object.values(x).every(validator); /** Returns true if and only if x is an array where each element matches the given validator. */ export const array = (validator: T) => (x: unknown): x is Array> => Array.isArray(x) && x.every(validator); /** Returns true if and only if x is an instance of the given type. */ export const instance = (base: T) => (x: unknown): x is InstanceOf => x instanceof base; // ------------------------------// // - - - SCHEMA VALIDATORS - - - // // ------------------------------// /** Returns true if and only if x is an object where each value matches the validator at the corresponding key in the schema. */ export const record = (schema: T) => (x: unknown): x is { [K in keyof T]: TypeOf } => isObject(x) && Object.entries(schema).every(([key, validate]) => validate(x[key])); /** Returns true if and only if x is an array where each element matches the validator at the corresponding index in the schema. */ export const tuple = (...schema: T) => (x: unknown): x is { [K in keyof T]: TypeOf } => Array.isArray(x) && schema.every((validator, i) => validator(x[i])); // ----------------------- // // - - - COMBINATORS - - - // // ----------------------- // /** Returns true if and only if x matches any of the given validators. */ export const any = (...validators: T) => (x: unknown): x is TypeOf => validators.some(validator => validator(x)); /** Returns true if and only if x matches all of the given validators. */ export const all = (...validators: T) => (x: unknown): x is UnionToIntersection> => validators.every(validator => validator(x)); /** Returns true if and only if x matches the given validator or is undefined. */ export const optional = (validator: Validator) => any(empty, validator); /** Returns true if and only if x matches the given validator or is null. */ export const nullable = (validator: Validator) => any(nil, validator); // --------------------- // // - - - REPORTING - - - // // --------------------- // const canProxy = (x: any): x is object => x !== null && (typeof x === "object" || typeof x === "function"); type Path = Array; function spy(source: T, report: (path: Path) => void, path: Path = []): T { if (!canProxy(source)) return source; return new Proxy(source, { get(target, property) { const value = (target as T)[property as keyof T]; const next = [...path, property]; report(next); return canProxy(value) ? spy(value, report, next) : value; } }); } /** Takes a validator and an object to be validated. Returns null if the object is valid, or the path to the invalid property. */ export function report( validate: (x: any) => boolean, object: any ): Path | null { let path: Path = []; const passed = validate(spy(object, current => (path = current))); return passed ? null : path; }