import { Cons, Nil } from "@tsplus/stdlib/collections/List/definition" /** * A Guard is a type representing the ability to identify when a value is of type A at runtime * * @tsplus type Guard * @tsplus derive nominal */ export interface Guard { readonly is: (u: unknown) => u is A } /** * @tsplus type GuardOps */ export interface GuardOps { (f: (u: unknown) => u is A): Guard } /** * Creates a new Guard */ export const Guard: GuardOps = (is) => ({ is }) // // Implicits // /** * Guard for `true` * * @tsplus implicit */ export const _true: Guard = Guard((u): u is true => u === true) /** * Guard for `false` * * @tsplus implicit */ export const _false: Guard = Guard((u): u is false => u === false) /** * Guard for a boolean * * @tsplus implicit */ export const boolean: Guard = Guard((u): u is boolean => typeof u === "boolean") /** * Guard for a number * * @tsplus implicit */ export const number: Guard = Guard((u): u is number => typeof u === "number") /** * Guard for null * * @tsplus implicit */ export const _null: Guard = Guard((u): u is null => u === null) /** * Guard for a string * * @tsplus implicit */ export const string: Guard = Guard((u): u is string => typeof u === "string") /** * Guard for a Date * * @tsplus implicit */ export const date: Guard = Guard((u): u is Date => u instanceof Date) /** * Guard for an object shaped like { _tag: string } * * @tsplus implicit */ export const taggedObject: Guard<{ _tag: string }> = Derive() // // Derivation Rules // /** * @tsplus derive Guard lazy */ export function deriveLazy( fn: (_: Guard) => Guard ): Guard { let cached: Guard | undefined const guard: Guard = Guard((u): u is A => { if (!cached) { cached = fn(guard) } return cached.is(u) }) return guard } /** * @tsplus derive Guard<_> 10 */ export function deriveNamed>( ...[base]: Check> extends Check.False ? [ base: Guard> ] : never ): Guard { // @ts-expect-error return base } /** * @tsplus derive Guard<_> 10 */ export function deriveValidation>( ...[base, brands]: Check> extends Check.True ? [ base: Guard>, brands: { [k in (keyof A[Brand.valid]) & string]: Brand.Validation } ] : never ): Guard { const validateBrands = Object.keys(brands).map((k) => brands[k]!) return Guard((u): u is A => base.is(u) && validateBrands.every((brand) => brand.validate(u as any)) ) } /** * @tsplus derive Guard<_> 20 */ export function deriveLiteral( ...[value]: Check & Check.Not>> extends Check.True ? [value: A] : never ): Guard { return Guard((u): u is A => u === value) } /** * @tsplus derive Guard[Maybe]<_> 10 */ export function deriveOption>( ...[element]: [A] extends [Maybe] ? [element: Guard<_A>] : never ): Guard { return Guard((u): u is A => { if (typeof u !== "object" || u == null) { return false } if (Equals.equals(Maybe.none, u)) { return true } const keys = Object.keys(u) if (keys.length !== 2) { return false } if ("_tag" in u && "value" in u && u["_tag"] === "Some") { return element.is(u["value"]) } return false }) } /** * @tsplus derive Guard[Chunk]<_> 10 */ export function deriveChunk>( ...[element]: [A] extends [Chunk] ? [element: Guard<_A>] : never ): Guard { return Guard((u): u is A => { if (Chunk.isChunk(u)) { return u.forAll(element.is) } return false }) } /** * @tsplus derive Guard[List]<_> 10 */ export function deriveList>( ...[element]: [A] extends [List] ? [element: Guard<_A>] : never ): Guard { return Guard((u): u is A => { if (u instanceof Cons || u instanceof Nil) { return (u as List).forAll(element.is) } return false }) } /** * @tsplus derive Guard[SortedSet]<_> 10 */ export function deriveSortedSet>( ...[element]: [A] extends [SortedSet] ? [element: Guard<_A>] : never ): Guard { return Guard((u): u is A => { if (SortedSet.isSortedSet(u)) { return u.forAll(element.is) } return false }) } /** * @tsplus derive Guard[NonEmptyImmutableArray]<_> 10 */ export function deriveNonEmptyImmutableArray>( ...[element]: [A] extends [NonEmptyImmutableArray] ? [element: Guard<_A>] : never ): Guard { return Guard((u): u is A => { if (u instanceof ImmutableArray && u.isNonEmpty()) { return u.array.every(element.is) } return false }) } /** * @tsplus derive Guard[ImmutableArray]<_> 10 */ export function deriveImmutableArray>( ...[element]: [A] extends [ImmutableArray] ? [element: Guard<_A>] : never ): Guard { return Guard((u): u is A => { if (u instanceof ImmutableArray) { return u.array.every(element.is) } return false }) } /** * @tsplus derive Guard[Array]<_> 10 */ export function deriveArray>( ...[element]: [A] extends [Array] ? Check>> extends Check.True ? [element: Guard<_A>] : never : never ): Guard { return Guard((u): u is A => { if (Array.isArray(u)) { return u.every(element.is) } return false }) } /** * @tsplus derive Guard[Either]<_> 10 */ export function deriveEither>( ...[left, right]: [A] extends [Either] ? [left: Guard<_E>, right: Guard<_A>] : never ): Guard { return Guard((u): u is A => { if (typeof u !== "object" || u == null) { return false } const keys = Object.keys(u) if (keys.length !== 2) { return false } if ("_tag" in u) { switch (u["_tag"]) { case "Left": { return "left" in u && left.is(u["left"]) } case "Right": { return "right" in u && right.is(u["right"]) } } } return false }) } /** * @tsplus derive Guard<_> 10 */ export function deriveEmptyRecord( ..._: Check> extends Check.True ? [] : never ): Guard { return Guard((u): u is A => typeof u === "object" && u !== null) } /** * @tsplus derive Guard<_> 20 */ export function deriveStruct>( ...[requiredFields, optionalFields]: Check> extends Check.True ? [ ...[ requiredFields: { [k in TypeLevel.RequiredKeys]: Guard } ], ...([TypeLevel.OptionalKeys] extends [never] ? [] : [ optionalFields: { [k in TypeLevel.OptionalKeys]: Guard> } ]) ] : never ): Guard { return Guard((u): u is A => { if (Derive>().is(u)) { for (const field of Object.keys(requiredFields)) { if (!(field in u) || !(requiredFields[field] as Guard).is(u[field])) { return false } } if (optionalFields) { for (const field of Object.keys(optionalFields)) { if ( field in u && typeof u[field] !== "undefined" && !(optionalFields[field] as Guard).is(u[field]) ) { return false } } } return true } return false }) } /** * @tsplus derive Guard<_> 15 */ export function deriveDictionary>( ...[valueGuard]: Check> extends Check.True ? [value: Guard] : never ): Guard { return Guard((u): u is A => { if (Derive>().is(u)) { for (const k of Object.keys(u)) { if (!valueGuard.is(u[k])) { return false } } return true } return false }) } /** * @tsplus derive Guard<_> 15 */ export function deriveRecord>( ...[value, requiredKeys]: Check> extends Check.True ? [ value: Guard, requiredKeys: { [k in keyof A]: 0 } ] : never ): Guard { const keys = new Set(Object.keys(requiredKeys)) return Guard((u): u is A => { const missing = new Set(Object.keys(requiredKeys)) if (Derive>().is(u)) { for (const k of Object.keys(u)) { if (keys.has(k) && !value.is(u[k])) { return false } missing.delete(k) } return missing.size === 0 } return false }) } /** * @tsplus derive Guard<_> 20 */ export function deriveTagged( ...[elements]: Check> extends Check.True ? [ elements: { [k in A["_tag"]]: Guard> } ] : never ): Guard { return Guard((u): u is A => { if (taggedObject.is(u)) { const guard = elements[u["_tag"] as A["_tag"]] if (guard) { return guard.is(u) } } return false }) } /** * @tsplus derive Guard<|> 30 */ export function deriveUnion( ...elements: { [k in keyof A]: Guard } ): Guard { return Guard((u): u is A[number] => { for (const element of elements) { if (element.is(u)) { return true } } return false }) } /** * @tsplus fluent Guard asserts */ export function asserts(self: Guard, u: unknown): asserts u is A { if (!self.is(u)) { throw new Error("Invalid assertion") } }