/** * Guard is a Typeclass for expressing Refinements * * @since 0.9.5 */ import { identity, pipe } from 'fp-ts/function' import { Refinement } from 'fp-ts/Refinement' import { Literal, memoize, Schemable1, WithRefine1, WithUnion1 } from './Schemable' // ------------------------------------------------------------------------------------- // Model // ------------------------------------------------------------------------------------- /** * @category Model * @since 0.9.5 */ export interface Guard { is: (i: I) => i is A } // ------------------------------------------------------------------------------------- // utils // ------------------------------------------------------------------------------------- /** * @since 2.2.2 */ export type TypeOf = G extends Guard ? A : never /** * @since 0.9.5 */ export type InputOf = G extends Guard ? I : never // ------------------------------------------------------------------------------------- // constructors // ------------------------------------------------------------------------------------- /** * @category constructors * @since 0.9.5 */ export const literal = ]>( ...values: A ): Guard => ({ is: (u: unknown): u is A[number] => values.findIndex((a) => a === u) !== -1, }) // ------------------------------------------------------------------------------------- // Decoder // ------------------------------------------------------------------------------------- /** * @category Decoder * @since 0.9.5 */ export const string: Guard = { is: (u: unknown): u is string => typeof u === 'string', } /** * Note: `NaN` is excluded. * * @category Decoder * @since 0.9.5 */ export const number: Guard = { is: (u: unknown): u is number => typeof u === 'number' && !isNaN(u), } /** * @category Decoder * @since 0.9.5 */ export const boolean: Guard = { is: (u: unknown): u is boolean => typeof u === 'boolean', } /** * @category Decoder * @since 0.9.5 */ export const date: Guard = { is: (u: unknown): u is Date => u instanceof Date, } /** * @category Decoder * @since 0.9.5 */ export const unknownArray: Guard> = { is: Array.isArray, } /** * @category Decoder * @since 0.9.5 */ export const unknownRecord: Guard> = { is: (u: unknown): u is Record => u !== null && typeof u === 'object' && !Array.isArray(u), } // ------------------------------------------------------------------------------------- // Combinator // ------------------------------------------------------------------------------------- /** * @category Combinator * @since 0.9.5 */ export const refine = (refinement: Refinement) => (from: Guard): Guard => ({ is: (i: I): i is B => from.is(i) && refinement(i), }) /** * @category Combinator * @since 0.9.5 */ export const nullable = (or: Guard): Guard => ({ is: (i): i is null | A => i === null || or.is(i), }) /** * @category Combinator * @since 2.2.15 */ export const struct = ( properties: { [K in keyof A]: Guard }, ): Guard => pipe( unknownRecord, refine( ( r, ): r is { [K in keyof A]: A[K] } => { for (const k in properties) { if (!(k in r) || !properties[k].is(r[k])) { return false } } return true }, ), ) /** * Use `struct` instead. * * @category Combinator * @since 0.9.5 * @deprecated */ export const type = struct /** * @category Combinator * @since 0.9.5 */ export const partial = ( properties: { [K in keyof A]: Guard }, ): Guard> => pipe( unknownRecord, refine((r): r is Partial => { for (const k in properties) { const v = r[k] if (v !== undefined && !properties[k].is(v)) { return false } } return true }), ) /** * @category Combinator * @since 0.9.5 */ export const array = (item: Guard): Guard> => pipe( unknownArray, refine((us): us is Array => us.every(item.is)), ) /** * @category Combinator * @since 0.9.5 */ export const record = (codomain: Guard): Guard> => pipe( unknownRecord, refine((r): r is Record => { for (const k in r) { if (!codomain.is(r[k])) { return false } } return true }), ) /** * @category Combinator * @since 0.9.5 */ export const tuple = >( ...components: { [K in keyof A]: Guard } ): Guard => ({ is: (u): u is A => Array.isArray(u) && u.length === components.length && components.every((c, i) => c.is(u[i])), }) /** * @category Combinator * @since 0.9.5 */ export const intersect = (right: Guard) => (left: Guard): Guard => ({ is: (u: unknown): u is A & B => left.is(u) && right.is(u), }) /** * @category Combinator * @since 0.9.5 */ export const union = (second: Guard) => (first: Guard): Guard => ({ is: (u: unknown): u is A | B => first.is(u) || second.is(u), }) /** * @category Combinator * @since 0.9.5 */ export const sum = (tag: T) => ( members: { [K in keyof A]: Guard> }, ): Guard => pipe( unknownRecord, refine((r): r is any => { const v = r[tag] as keyof A if (v in members) { return members[v].is(r) } return false }), ) /** * @category Combinator * @since 0.9.5 */ export const lazy = (f: () => Guard): Guard => { const get = memoize>(f) return { is: (u: unknown): u is A => get().is(u), } } /** * @category Combinator * @since 2.2.15 */ export const readonly: (guard: Guard) => Guard> = identity /** * @category Combinator * @since 0.9.5 */ export const alt = (that: () => Guard) => (me: Guard): Guard => ({ is: (i): i is A => me.is(i) || that().is(i), }) /** * @category Combinator * @since 0.9.5 */ export const zero = (): Guard => ({ is: (_): _ is A => false, }) /** * @category Combinator * @since 0.9.5 */ export const compose = (to: Guard) => (from: Guard): Guard => ({ is: (i): i is B => from.is(i) && to.is(i), }) /** * @category Combinator * @since 0.9.5 */ export const id = (): Guard => ({ is: (_): _ is A => true, }) // ------------------------------------------------------------------------------------- // instances // ------------------------------------------------------------------------------------- /** * @category instances * @since 0.9.5 */ export const URI = '@typed/fp/Guard' /** * @category instances * @since 0.9.5 */ export type URI = typeof URI declare module 'fp-ts/HKT' { interface URItoKind { readonly [URI]: Guard } } /** * @category instances * @since 0.9.5 */ export const Schemable: Schemable1 = { URI, literal, string, number, boolean, date, nullable, struct, record, array, tuple: tuple as Schemable1['tuple'], intersect, sum, lazy: (_, f) => lazy(f), branded: identity as Schemable1['branded'], unknownArray, unknownRecord, } /** * @category instances * @since 0.9.5 */ export const WithUnion: WithUnion1 = { union, } /** * @category instances * @since 0.9.5 */ export const WithRefine: WithRefine1 = { refine: refine as WithRefine1['refine'], }