// A type-first validator with full typing support. type MakeUndefinedOptional = { [K in keyof T as undefined extends T[K] ? never : K]: T[K] } & { [K in keyof T as undefined extends T[K] ? K : never]?: T[K] } type UnwrapValidatorRecord = { [K in keyof T]: T[K] extends Validator ? U : never } export class Validator { constructor(public check: (value: any) => value is T) {} debug(value: any) { console.log(value, this.check(value)) } or(other: Validator) { return new Validator( (value): value is T | U => this.check(value) || other.check(value), ) } and(other: Validator) { return new Validator( (value): value is T & U => this.check(value) && other.check(value), ) } not(): Validator not(other: Validator): Validator> not(other?: Validator) { if (other) { return new Validator( (value): value is Exclude => this.check(value) && !other.check(value), ) } else { return new Validator( (value): value is unknown => !this.check(value), ) } } optional(): Validator { return this.or(undefined()) } } export class ArrayValidator extends Validator { constructor(protected itemValidator: Validator = any()) { super( (value): value is T[] => Array.isArray(value) && value.every((element) => itemValidator.check(element)), ) } } export class ObjectValidator< T extends Record>, > extends Validator<{ [K in keyof MakeUndefinedOptional< UnwrapValidatorRecord >]: MakeUndefinedOptional>[K] }> { protected object: [string | number | symbol, Validator][] constructor(object: T) { super( (value): value is MakeUndefinedOptional> => typeof value === "object" && value !== null && this.object.every(([k, v]) => v.check(value[k])), ) this.object = Object.entries(object) } } type Primitives = { bigint: bigint boolean: boolean number: number string: string symbol: symbol undefined: undefined } type Primitive = bigint | boolean | null | number | string | symbol | undefined export function primitive(type: "bigint"): Validator export function primitive(type: "boolean"): Validator export function primitive(type: "number"): Validator export function primitive(type: "string"): Validator export function primitive(type: "char"): Validator export function primitive(type: "undefined"): Validator export function primitive(type: T) { return new Validator( (value): value is Primitives[T] => typeof value === type, ) } export const bigint = () => primitive("bigint") export const boolean = () => primitive("boolean") export const number = () => primitive("number") export const string = () => primitive("string") export const symbol = () => primitive("char") export const undefined = () => primitive("undefined") const _null = () => new Validator((value): value is null => value === null) export { _null as null } const _function = () => new Validator( ( value, ): value is ((...args: any[]) => any) | (new (...args: any[]) => any) => typeof value === "function", ) export { _function as function } export const any = () => new Validator((_value): _value is any => true) export const never = () => new Validator((_value): _value is never => false) export const is = (value: T) => new Validator((_value): _value is T => _value === value) export function array(item?: Validator) { return new ArrayValidator(item) } export namespace array { export function empty() { return new ArrayValidator(never()) } } export function object< T extends Record>, >(object: T) { return new ObjectValidator(object) } const myObj = object({ a: is(2).optional(), b: string(), c: array(boolean()), }) const b = null! as any if (myObj.check(b)) { b.a }