Source

guard.ts

import { narrow, Narrower, UnNarrow } from './narrow'

/**
 * Creates a function from a narrower schema that can be reused to narrow objects.
 * This simple closure can be used when a whole Guard instance would be too much.
 *
 * @example
 * import { satisfier } from 'narrow-minded'
 * const satisfies = satisfier(['string', 'number'])
 * satisfies(['horse', 42]) // => true
 */
export const satisfier =
	<N extends Narrower>(n: N) =>
	(u: unknown): u is UnNarrow<N> =>
		narrow(n, u)

export type NarrowingFunction<P> = (u: unknown) => u is P
export type Payload<G> = G extends Guard<infer P> ? P : unknown

/**
 *
 */
export class Guard<P> {
	/**
	 * Creates a new guard that uses a `narrow` function.
	 * A little shortcut for `new Guard(narrow(...))`.
	 * @example
	 *
	 * import { Guard } from 'narrow-minded'
	 * const myGuard = Guard.narrow(['string', 'number'])
	 * myGuard.satisfied(['horse', 42]) // => true
	 *
	 * @param n Narrower
	 * @returns Guard
	 */
	static narrow<N extends Narrower>(n: N) {
		return new Guard((u: unknown): u is UnNarrow<N> => narrow(n, u))
	}

	readonly NF: NarrowingFunction<P>

	constructor(NF: NarrowingFunction<P>) {
		this.NF = NF
	}

	/**
	 * Runs the guard's narrowing function to validate the unknown value's type.
	 * Operates as a type predicate so conditional blocks infer this structure.
	 * @example
	 *
	 * const myGuard = Guard.narrow({
	 * 	name: 'string',
	 * 	values: ['number'],
	 * })
	 *
	 * const good: unknown = { name: 'Horse', values: [1, 2] }
	 * if (myGuard.satisfied(good)) {
	 * 	console.log('Good ' + good.name)
	 * 	// => 'Good Horse'
	 * }
	 *
	 * const bad: unknown = { name: 42, values: 'Nope' }
	 * if (!myGuard.satisfied(bad)) {
	 * 	console.log('Bad ')
	 * 	// => 'Bad'
	 * }
	 *
	 * @param u The unknown value.
	 * @returns A type predicate that `u` satisfies this guard.
	 */
	satisfied(u: unknown): u is P {
		return this.NF(u)
	}

	/**
	 * An identity function that returns the value passed to it. Useful for
	 * defining objects that satisfy this guard using type inference.
	 * @param p
	 * @returns p
	 */
	build(p: P): P {
		return p
	}

	/**
	 * Creates a new guard that will satisfy the constraints of `this` AND `other`.
	 * Useful for combining primitive narrows with more complex type checking.
	 * @example
	 * const myGuard = Guard.narrow({ type: 'string' }).and(
	 * 	(u: unknown): u is { type: 'this' | 'that' } =>
	 * 		['this', 'that'].includes((u as any).type),
	 * )
	 *
	 * @param other - Another Guard or a Narrower/NarrowerFunction which will
	 * be wrapped into a Guard automatically.
	 * @return Guard
	 */
	and<N extends Narrower>(other: N): Guard<P & UnNarrow<N>>
	and<P2>(other: Guard<P2> | NarrowingFunction<P2>): Guard<P & P2>
	and<P2>(other: any) {
		const left = this.NF
		const right =
			other instanceof Guard
				? other.NF
				: other instanceof Function
				? other
				: (u: unknown): u is P2 => narrow(other, u)
		return new Guard((u: unknown): u is P & P2 => left(u) && right(u))
	}
}

/**
 * A singleton that can be used to build `and` chains.
 * @example
 * if (unknown.and('string').satisfied('Great')) {
 * 	console.log('Great')
 * }
 */
export const unknown = new Guard<unknown>((_): _ is unknown => true)