|
/*! |
|
* Copyright (c) 2024-2025 Nicholas Berlette. All rights reserved. |
|
* MIT License (https://nick.mit-license.org/2024) |
|
*/ |
|
|
|
// deno-lint-ignore-file no-explicit-any ban-types no-unused-vars |
|
|
|
import { inspect, type InspectOptionsStylized } from "node:util"; |
|
import { lru } from "jsr:@decorators/lru@0.1.2"; |
|
import { alias } from "jsr:@decorators/alias@0.1.2"; |
|
import { |
|
isArray, |
|
isArrayBuffer, |
|
isAsyncFunction, |
|
isAsyncGenerator, |
|
isAsyncGeneratorFunction, |
|
isAsyncIterable, |
|
isAsyncIterableIterator, |
|
isAsyncIterator, |
|
isBigInt64Array, |
|
isBigUint64Array, |
|
isDataView, |
|
isDate, |
|
isDateString, |
|
isError, |
|
isFinite, |
|
isFloat, |
|
isFloat16Array, |
|
isFloat32Array, |
|
isFloat64Array, |
|
isFunction, |
|
isGenerator, |
|
isGeneratorFunction, |
|
isInt16Array, |
|
isInt32Array, |
|
isInt8Array, |
|
isInteger, |
|
isIterable, |
|
isIterableIterator, |
|
isMap, |
|
isNegative, |
|
isNonZero, |
|
isNull, |
|
isObject, |
|
isPlainObject, |
|
isPositive, |
|
isPrimitive, |
|
isPromise, |
|
isPropertyKey, |
|
isRegExp, |
|
isRegisteredSymbol, |
|
isSet, |
|
isString, |
|
isTemplateStringsArray, |
|
isTypedArray, |
|
isUint16Array, |
|
isUint32Array, |
|
isUint8Array, |
|
isUint8ClampedArray, |
|
isUniqueSymbol, |
|
isURLString, |
|
isWeakKey, |
|
isWeakMap, |
|
isWeakRef, |
|
isWeakSet, |
|
isWellKnownSymbol, |
|
type RegisteredSymbol, |
|
type TemplateStringsArray, |
|
type TypedArrayTypeMap, |
|
type TypedArrayTypeName, |
|
type UniqueSymbol, |
|
type WellKnownSymbol, |
|
} from "./guards.ts"; |
|
import { |
|
FunctionPrototype, |
|
FunctionPrototypeApply, |
|
FunctionPrototypeCall, |
|
ObjectDefineProperties, |
|
ObjectFreeze, |
|
ObjectGetOwnPropertyDescriptors, |
|
ObjectSetPrototypeOf, |
|
Proxy, |
|
ReflectGet, |
|
ReflectHas, |
|
ReflectOwnKeys, |
|
SymbolAsyncDispose, |
|
SymbolAsyncIterator, |
|
SymbolDispose, |
|
SymbolHasInstance, |
|
SymbolIsConcatSpreadable, |
|
SymbolIterator, |
|
SymbolMatch, |
|
SymbolMatchAll, |
|
SymbolMetadata, |
|
SymbolReplace, |
|
SymbolSearch, |
|
SymbolSpecies, |
|
SymbolSplit, |
|
SymbolToPrimitive, |
|
SymbolToStringTag, |
|
SymbolUnscopables, |
|
} from "./primordials.ts"; |
|
import { getNoColor } from "./env.ts"; |
|
|
|
type strings = string & {}; |
|
|
|
export interface RenderOptions { |
|
indent?: number; |
|
indentWidth?: number; |
|
lineWidth?: number; |
|
useTabs?: boolean; |
|
trailingComma?: boolean; |
|
semiColons?: boolean; |
|
singleQuote?: boolean; |
|
} |
|
|
|
const defaultRenderOptions = { |
|
indent: 0, |
|
indentWidth: 2, |
|
lineWidth: 80, |
|
useTabs: false, |
|
semiColons: true, |
|
trailingComma: true, |
|
singleQuote: false, |
|
} as const satisfies RenderOptions; |
|
|
|
type Id<T> = T; |
|
|
|
class Callable< |
|
This = unknown, |
|
Args extends readonly unknown[] = any[], |
|
Return = any, |
|
> { |
|
constructor( |
|
factory: (this: This, ...args: Args) => Return, |
|
) { |
|
function fn(this: This, ...args: Args): Return { |
|
return FunctionPrototypeCall(factory, this, ...args); |
|
} |
|
const proto = new Proxy(FunctionPrototype, { |
|
getPrototypeOf: () => new.target.prototype, |
|
setPrototypeOf: () => true, |
|
get: (t, p, r) => { |
|
if (p === "constructor") return new.target; |
|
if (p === "prototype") return t.prototype; |
|
if (p in new.target.prototype) { |
|
return ReflectGet(new.target.prototype, p, r); |
|
} |
|
if (p === "name") return new.target.name; |
|
return ReflectGet(t, p, r); |
|
}, |
|
has: (t, p) => ReflectHas(new.target.prototype, p) || ReflectHas(t, p), |
|
ownKeys: ( |
|
t, |
|
) => [ |
|
...new Set( |
|
ReflectOwnKeys(t).concat(ReflectOwnKeys(new.target.prototype)), |
|
), |
|
], |
|
}); |
|
|
|
ObjectSetPrototypeOf(fn, proto); |
|
ObjectDefineProperties(fn, ObjectGetOwnPropertyDescriptors(this)); |
|
|
|
return fn as never; |
|
} |
|
|
|
static { |
|
ObjectSetPrototypeOf(this, Function); |
|
} |
|
} |
|
|
|
const { colors, styles } = inspect; |
|
const colorize = <T extends keyof typeof colors>(val: string, color: T) => { |
|
const [before, after] = colors[color] ?? []; |
|
if (before != null && after != null) { |
|
return `\x1b[${before}m${val}\x1b[${after}m`; |
|
} else { |
|
return val; |
|
} |
|
}; |
|
const stylize: InspectOptionsStylized["stylize"] = (text, style) => { |
|
if (style in styles) return colorize(text, styles[style]); |
|
return text; |
|
}; |
|
|
|
const defaultInspectOptions = { |
|
colors: !getNoColor(), |
|
compact: true, |
|
breakLength: 100, |
|
getters: "get", |
|
maxArrayLength: 10, |
|
maxStringLength: 60, |
|
sorted: true, |
|
numericSeparator: true, |
|
customInspect: true, |
|
showHidden: false, |
|
showProxy: false, |
|
depth: 0, |
|
stylize, |
|
} satisfies InspectOptionsStylized; |
|
|
|
// #endregion internal |
|
|
|
export interface Err<E = string> { |
|
readonly success: false; |
|
readonly error: E; |
|
readonly path: PropertyKey[]; |
|
readonly errors?: readonly ValidationError[] | undefined; |
|
} |
|
|
|
export interface Ok<T = any> { |
|
readonly success: true; |
|
readonly value: T; |
|
} |
|
|
|
export type Result<T, E = string> = Ok<T> | Err<E>; |
|
|
|
export interface ValidationErrorOptions<T = any, E = string> { |
|
cause?: unknown; |
|
error?: E; |
|
errors?: (E | ValidationError<T, E>)[] | undefined; |
|
path?: PropertyKey[]; |
|
type?: Type<T> | null; |
|
} |
|
|
|
type ResolvedValidationErrorOptions<T, E> = Required< |
|
ValidationErrorOptions<T, E> |
|
>; |
|
|
|
// deno-fmt-ignore |
|
const unicode = { |
|
TL: "┌", TH: "─", TM: "┬", TR: "┐", |
|
ML: "├", MH: "─", MM: "┼", MR: "┤", |
|
LL: "│", LH: " ", LM: "├", LR: "│", |
|
BL: "└", BH: "─", BM: "┴", BR: "┘", |
|
V0: "╵", V2: "╎", V3: "┆", V4: "┊", |
|
H0: "╴", H2: "╌", H3: "┄", H4: "┈", |
|
} as const; |
|
|
|
export class ValidationError<T = any, E = any> extends Error implements Err<E> { |
|
static from<T, E = any>( |
|
error: E, |
|
options?: ValidationErrorOptions<T, E>, |
|
): ValidationError<T, E>; |
|
static from<T, E = any>( |
|
error: ValidationError<T, E> | { error: E; path: PropertyKey[] }, |
|
options?: ValidationErrorOptions<T, E>, |
|
): ValidationError<T, E>; |
|
static from<T, E = any>( |
|
options: ValidationErrorOptions<T, E>, |
|
): ValidationError<T, E>; |
|
static from( |
|
error: any, |
|
options?: ValidationErrorOptions<any, any>, |
|
): ValidationError<any, any> { |
|
if (error instanceof ValidationError) { |
|
return new ValidationError(error.error, { |
|
...error.#options, |
|
...options, |
|
cause: error, |
|
}); |
|
} |
|
if (error && typeof error === "object") { |
|
const { error: e, errors = [], path = [], type = null } = error; |
|
return new ValidationError(e, { ...options, path, type, errors }); |
|
} |
|
return new ValidationError(error, options); |
|
} |
|
|
|
#options = {} as ResolvedValidationErrorOptions<T, E>; |
|
|
|
constructor( |
|
message: E, |
|
options: ValidationErrorOptions<T, E> = {}, |
|
) { |
|
const { cause, type = null, error = message, errors = [], path = [] } = |
|
options; |
|
super(error + "", { cause }); |
|
this.name = "ValidationError"; |
|
this.#options = { cause, error, errors, path, type }; |
|
} |
|
|
|
get success(): false { |
|
return false; |
|
} |
|
|
|
get error(): E { |
|
return this.#options.error!; |
|
} |
|
|
|
get errors(): ValidationError<T, E>[] { |
|
return this.#options.errors?.map((e) => |
|
ValidationError.from<T, E>(e as E) |
|
) ?? []; |
|
} |
|
|
|
get path(): PropertyKey[] { |
|
return this.#options.path ??= []; |
|
} |
|
|
|
get type(): Type<T> | null { |
|
return this.#options.type!; |
|
} |
|
|
|
override toString(): string { |
|
const { cause, error, errors = [], type, path = ["<root>"] } = |
|
this.#options; |
|
let msg = ""; |
|
if (error) msg += String(error); |
|
if (path.length) { |
|
msg += `\n \x1b[0;2;3mat \x1b[23;4:3;58;2;68;34;109m${ |
|
path.join(".") |
|
}\x1b[0m`; |
|
} |
|
if (type && type instanceof Type) { |
|
msg += |
|
`\n \x1b[0;1;4;58;5;54mexpected type\x1b[0;2m:\x1b[0m ${type.name}`; |
|
} |
|
if (cause && isError(cause) && cause !== this) { |
|
msg += "\n \x1b[0;1;4;58;5;93mcaused by\x1b[0;2m:\x1b[0m"; |
|
} |
|
|
|
if (errors.length) { |
|
msg += "\n \x1b[0;1;4;58;5;208mvalidation errors\x1b[0;2m:\x1b[0m"; |
|
let before = "", after = ""; |
|
for (let i = 0; i < errors.length; i++) { |
|
const e = errors[i]; |
|
const indent = " "; |
|
let text = ""; |
|
let guide = `${indent}${unicode.LL} `; |
|
const lines = String(e).split(/\r?\n/g); |
|
for (let j = 0; j < lines.length; j++) { |
|
const line = lines[j]; |
|
// use a guide with a branch to the right (first line only) |
|
if (j === 0 && i === 0) { |
|
guide = `${indent}${unicode.ML}${unicode.H0}`; |
|
before += `${indent}${unicode.TL}${ |
|
unicode.TH.repeat(94) |
|
}${unicode.H2}${unicode.H3}${unicode.H4}\n`; |
|
} else if (j === lines.length - 1 && i === errors.length - 1) { |
|
guide = `${indent}${unicode.ML}${unicode.H0}`; |
|
after += `${indent}${unicode.BL}${ |
|
unicode.BH.repeat(94) |
|
}${unicode.H2}${unicode.H3}${unicode.H4}\n`; |
|
} else { |
|
guide = `${indent}${unicode.LL} `; |
|
} |
|
text += line.replace(/^/mg, `\x1b[2m${guide}\x1b[0m`) + "\n"; |
|
} |
|
msg += `\n${before}${text}${after}`; |
|
} |
|
} |
|
return msg; |
|
} |
|
} |
|
|
|
const ErrPrototype = { |
|
__proto__: null!, |
|
toString: function (this: Err<any>): string { |
|
return String(this.error); |
|
}, |
|
[Symbol.for("nodejs.util.inspect.custom")](): string { |
|
return this.toString(); |
|
}, |
|
get [SymbolToStringTag](): string { |
|
return "ValidationError"; |
|
}, |
|
} as unknown as Err<any> & ThisType<Err<any>>; |
|
|
|
export type ValidationResult<T = any, E = string | Error> = Ok<T> | Err<E>; |
|
|
|
type ErrorLike<T> = |
|
| string |
|
| { error: unknown; path: PropertyKey[] } |
|
| ValidationError<T>; |
|
|
|
export class Context<T = any, I = any, O = T> { |
|
#errors: ErrorLike<T>[] = []; |
|
|
|
constructor( |
|
readonly path: PropertyKey[] = [], |
|
readonly type: Type<T> | null = null, |
|
) {} |
|
|
|
get errors(): ValidationError<T>[] { |
|
const { path, type } = this; |
|
return this.#errors.map((e) => ValidationError.from<T>(e, { path, type })); |
|
} |
|
|
|
add(...errors: ErrorLike<T>[]): this { |
|
this.#errors.push(...errors); |
|
this.#errors = this.#errors.filter((e, i, a) => |
|
e != null && a.indexOf(e) === i |
|
); |
|
return this; |
|
} |
|
|
|
ok(value: T): Ok<T> { |
|
return { __proto__: null!, success: true, value } as Ok<T>; |
|
} |
|
|
|
/** |
|
* Creates a contextualized {@linkcode ValidationResult} error with a given |
|
* message and optional subpath. If provided, the path will be appended to |
|
* the error message in dot notation, relative to the current context path. |
|
* |
|
* @param message - The error message or an `Error` object. |
|
* @param [path] - An optional subpath to append to the error message. |
|
* @returns A new {@linkcode ValidationResult} object with `success: false`. |
|
*/ |
|
err<E extends string | Error>( |
|
message: E, |
|
path?: PropertyKey[], |
|
): Err<E>; |
|
/** |
|
* Creates a contextualized {@linkcode ValidationResult} error from the |
|
* provided template string literal and values. The interpolated template |
|
* values are each appended to the template after being rendered with the |
|
* `inspect` function from the `node:util` module. |
|
* |
|
* @param template - The template string literal. |
|
* @param values - The values to interpolate into the template. |
|
* @returns A new {@linkcode ValidationResult} object with `success: false`. |
|
* @example |
|
* ```ts |
|
* import * as t from "@nick/lint-core/runtime-types"; |
|
* |
|
* class MyType<T> extends t.Type<T> { |
|
* constructor(override name: string, readonly value: T) { |
|
* super(name, { value, validator: (x): x is T => x === value }); |
|
* } |
|
* |
|
* validate(v: unknown, ctx: t.Context): t.ValidationResult<T> { |
|
* if (!this.is(v)) { |
|
* return ctx.err`Expected ${this}, got {v} ({typeof v})`; |
|
* } |
|
* return ctx.ok(v); |
|
* } |
|
* } |
|
* |
|
* const six = new MyType("six", 6); |
|
* const result = six.decode(5); |
|
* |
|
* console.log(result.error); |
|
* // Expected [MyType<six>], got 5 (type: 'number') |
|
* ``` |
|
*/ |
|
err<E = any>( |
|
template: TemplateStringsArray, |
|
...values: unknown[] |
|
): Err<E>; |
|
/** @internal */ |
|
err<E>( |
|
message: TemplateStringsArray | string | Error, |
|
...paths: unknown[] |
|
): Err<E> { |
|
let path: PropertyKey[] = []; |
|
if (isTemplateStringsArray(message)) { |
|
const options = { ...defaultInspectOptions }; |
|
let str = ""; |
|
for (let i = 0; i < message.length; i++) { |
|
str += message[i]; |
|
if (i < path.length) str += Type.inspect(path[i], options); |
|
} |
|
message = str; |
|
path.length = 0; |
|
} |
|
path = [...this.path, ...path]; |
|
if (!path.length) path.push("<root>"); |
|
let error = message.toString(); |
|
if (isError(message)) { |
|
const e = message; |
|
error = `${e.message || message}`; |
|
const path = [...this.path]; |
|
if ("path" in e && isArray(e.path, isPropertyKey)) { |
|
path.push(...e.path); |
|
} |
|
if (path.length > 0) error += `\n at ${path.join(".")}`; |
|
if (e.cause) { |
|
error += `\nCaused by:\n${String(e.cause).replace(/^/mg, " ╎ ")}`; |
|
} |
|
} else if (this.path.length > 0) { |
|
error += `\n at ${this.path.join(".")}`; |
|
} |
|
const errors = [...this.errors]; |
|
return new ValidationError(error, { |
|
path, |
|
type: this.type, |
|
errors, |
|
cause: message, |
|
}); |
|
} |
|
|
|
/** |
|
* Creates a new context from the provided path, such that the new instance |
|
* is relative to the path of the current instance (i.e., a child context). |
|
* |
|
* The subpath can be a single string or an array of strings. If an array is |
|
* provided, it will be flattened and concatenated with the parent path. |
|
* |
|
* @param path - The path to append to the current context. |
|
* @returns A new {@linkcode Context} object with the merged path. |
|
* @example |
|
* ```ts |
|
* |
|
* ``` |
|
*/ |
|
with(...path: PropertyKey[] | [PropertyKey[]]): Context<T, I, O> { |
|
return new Context([...this.path, ...path].flat()); |
|
} |
|
} |
|
|
|
const identity = <T>(x: T): T => x; |
|
|
|
export type TypeConstraint< |
|
U extends T, |
|
T = unknown, |
|
K extends PropertyKey = PropertyKey, |
|
> = { |
|
readonly name: K; |
|
predicate(value: T): value is U; |
|
message?: string; |
|
} | { |
|
readonly name: K; |
|
predicate(value: T): unknown; |
|
message?: string; |
|
}; |
|
|
|
// ---------------------------------------------------------------------------- |
|
// TypeInfo |
|
// ---------------------------------------------------------------------------- |
|
export interface TypeInfoState<V = unknown, T extends Type<V> = Type<V>> { |
|
name: string; |
|
type: T; |
|
args: ConstructorParameters<T["constructor"]>; |
|
default: V; |
|
optional: boolean; |
|
nullable: boolean; |
|
description: string; |
|
constraints: TypeConstraint<V>[]; |
|
validator(input: unknown): input is V; |
|
|
|
[key: PropertyKey]: any; |
|
} |
|
|
|
const defaultTypeInfoState = { |
|
name: "any", |
|
type: undefined! as Any, |
|
args: [] as any[], |
|
default: undefined as any, |
|
optional: false, |
|
nullable: false, |
|
description: "Any type", |
|
constraints: [], |
|
validator: (input): input is any => (void input, true), |
|
} as const satisfies TypeInfoState; |
|
|
|
export type DefaultTypeInfoState = typeof defaultTypeInfoState; |
|
|
|
type WithTypeInfo< |
|
V, |
|
T extends Type<V>, |
|
S extends TypeInfoState<V, T | Type<V>>, |
|
I extends Partial<TypeInfoState<V>>, |
|
> = TypeInfo< |
|
V, |
|
T, |
|
TypeInfoState<V, T> & Pick<S, Exclude<keyof S, keyof I>> & I |
|
> extends infer V ? V : never; |
|
|
|
/** |
|
* The `TypeInfo` class is used internally to store metadata and additional type |
|
* information on instances of {@linkcode Type} subclasses. It provides support |
|
* for managing extra type constraints, custom error messages, human-readable |
|
* names/descriptions, and various other parts of the different states of a |
|
* given Type instance. |
|
* |
|
* @internal |
|
*/ |
|
export class TypeInfo< |
|
V, |
|
T extends Type<V> = Type<V>, |
|
S extends TypeInfoState<V, T> = TypeInfoState<V, T>, |
|
> { |
|
constructor( |
|
readonly _: S = { ...defaultTypeInfoState } as DefaultTypeInfoState & S, |
|
) {} |
|
|
|
get state(): S { |
|
return this._; |
|
} |
|
|
|
name<K extends string>( |
|
typeName: K, |
|
): WithTypeInfo<V, T, S, { name: K }> { |
|
this._.name = typeName; |
|
return this as never; |
|
} |
|
|
|
type<U extends Type<any> = Type<V>>( |
|
type: U, |
|
): WithTypeInfo<V, T & U, S, { type: U }> { |
|
(this as TypeInfo<any>)._.type = type; |
|
return this as never; |
|
} |
|
|
|
default<K extends V>(value: K): WithTypeInfo<V, T, S, { default: K }> { |
|
this._.default = value; |
|
return this as never; |
|
} |
|
|
|
optional<O extends boolean = true>( |
|
optional: O = true as O, |
|
): WithTypeInfo<V, T, S, { optional: O }> { |
|
this._.optional = !!optional; |
|
return this as never; |
|
} |
|
|
|
nullable<N extends boolean = true>( |
|
nullable: N = true as N, |
|
): WithTypeInfo<V, T, S, { nullable: N }> { |
|
this._.nullable = !!nullable; |
|
return this as never; |
|
} |
|
|
|
description<K extends string>( |
|
description: K, |
|
): WithTypeInfo<V, T, S, { description: K }> { |
|
this._.description = description; |
|
return this as never; |
|
} |
|
|
|
validator<K extends (input: unknown) => input is V>( |
|
validator: K, |
|
): WithTypeInfo<V, T, S, { validator: K }> { |
|
this._.validator = validator; |
|
return this as never; |
|
} |
|
|
|
constraint<K extends PropertyKey, U extends V>( |
|
name: K, |
|
predicate: (value: V) => value is U, |
|
message?: string, |
|
): WithTypeInfo< |
|
V, |
|
T, |
|
S, |
|
{ constraints: [...S["constraints"], TypeConstraint<U, V, K>] } |
|
>; |
|
constraint<K extends PropertyKey>( |
|
name: K, |
|
predicate: (value: V) => unknown, |
|
message?: string, |
|
): WithTypeInfo< |
|
V, |
|
T, |
|
S, |
|
{ constraints: [...S["constraints"], TypeConstraint<V, V, K>] } |
|
>; |
|
constraint<K extends PropertyKey>( |
|
name: K, |
|
predicate: (value: V) => unknown, |
|
message?: string, |
|
): WithTypeInfo< |
|
V, |
|
T, |
|
S, |
|
{ constraints: [...S["constraints"], TypeConstraint<V, V, K>] } |
|
> { |
|
return this.constraints([...this._.constraints, { |
|
name, |
|
predicate, |
|
message, |
|
}]) as never; |
|
} |
|
|
|
constraints<K extends TypeConstraint<V>[]>( |
|
constraints: K, |
|
): WithTypeInfo<V, T, S, { constraints: K }> { |
|
this._.constraints = constraints; |
|
return this as never; |
|
} |
|
|
|
getName(): S["name"] { |
|
return this._.name; |
|
} |
|
|
|
getType(): S["type"] { |
|
return this._.type; |
|
} |
|
|
|
getDefault(): S["default"] { |
|
return this._.default; |
|
} |
|
|
|
getOptional(): S["optional"] { |
|
return this._.optional; |
|
} |
|
|
|
getNullable(): S["nullable"] { |
|
return this._.nullable; |
|
} |
|
|
|
getDescription(): S["description"] { |
|
return this._.description; |
|
} |
|
|
|
getValidator(): S["validator"] { |
|
return this._.validator; |
|
} |
|
|
|
getConstraints(): S["constraints"] { |
|
return this._.constraints; |
|
} |
|
|
|
hasName(): this is WithTypeInfo<V, T, S, { name: NonNullable<S["name"]> }>; |
|
hasName<K extends string>( |
|
name: K, |
|
): this is WithTypeInfo<V, T, S, { name: K }>; |
|
hasName(name?: string) { |
|
return this._.name != null && (!name || this._.name === name); |
|
} |
|
|
|
hasType(): this is WithTypeInfo<V, T, S, { type: NonNullable<S["type"]> }>; |
|
hasType<K extends Type<V>>( |
|
type: K, |
|
): this is WithTypeInfo<V, T, S, { type: K }>; |
|
hasType(type?: Type<V>) { |
|
return this._.type != null && (!type || this._.type === type); |
|
} |
|
|
|
hasDefault(): this is WithTypeInfo< |
|
V, |
|
T, |
|
S, |
|
{ default: NonNullable<S["default"]> } |
|
>; |
|
hasDefault<K extends V>( |
|
value: K, |
|
): this is WithTypeInfo<V, T, S, { default: K }>; |
|
hasDefault(value?: V) { |
|
return this._.default != null && (!value || this._.default === value); |
|
} |
|
|
|
hasOptional(): this is WithTypeInfo< |
|
V, |
|
T, |
|
S, |
|
{ optional: NonNullable<S["optional"]> } |
|
>; |
|
hasOptional<K extends boolean>( |
|
optional: K, |
|
): this is WithTypeInfo<V, T, S, { optional: K }>; |
|
hasOptional(optional?: boolean) { |
|
return this._.optional != null && |
|
(!optional || this._.optional === optional); |
|
} |
|
|
|
hasNullable(): this is WithTypeInfo< |
|
V, |
|
T, |
|
S, |
|
{ nullable: NonNullable<S["nullable"]> } |
|
>; |
|
hasNullable<K extends boolean>( |
|
nullable: K, |
|
): this is WithTypeInfo<V, T, S, { nullable: K }>; |
|
hasNullable(nullable?: boolean) { |
|
return this._.nullable != null && |
|
(!nullable || this._.nullable === nullable); |
|
} |
|
|
|
hasDescription(): this is WithTypeInfo< |
|
V, |
|
T, |
|
S, |
|
{ description: NonNullable<S["description"]> } |
|
>; |
|
hasDescription<K extends string>( |
|
description: K, |
|
): this is WithTypeInfo<V, T, S, { description: K }>; |
|
hasDescription(description?: string) { |
|
return this._.description != null && |
|
(!description || this._.description === description); |
|
} |
|
|
|
hasValidator(): this is WithTypeInfo< |
|
V, |
|
T, |
|
S, |
|
{ validator: NonNullable<S["validator"]> } |
|
>; |
|
hasValidator<K extends (input: unknown) => input is V>( |
|
validator: K, |
|
): this is WithTypeInfo<V, T, S, { validator: K }>; |
|
hasValidator(validator?: (input: unknown) => input is V) { |
|
return this._.validator != null && |
|
(!validator || this._.validator === validator); |
|
} |
|
|
|
hasConstraints(): this is WithTypeInfo< |
|
V, |
|
T, |
|
S, |
|
{ constraints: NonNullable<S["constraints"]> } |
|
>; |
|
hasConstraints<K extends S["constraints"]>( |
|
constraints: K, |
|
): this is WithTypeInfo<V, T, S, { constraints: K }>; |
|
hasConstraints(constraints?: S["constraints"]) { |
|
return this._.constraints != null && |
|
(!constraints || this._.constraints === constraints); |
|
} |
|
} |
|
|
|
const _type: unique symbol = Symbol("type"); |
|
type _type = typeof _type; |
|
|
|
const _spec: unique symbol = Symbol("spec"); |
|
type _spec = typeof _spec; |
|
|
|
function deepClone<const T>(arg: T): T; |
|
function deepClone(arg: any): any { |
|
if (!arg || isPrimitive(arg)) return arg; |
|
if (arg instanceof Type) return arg.clone(); |
|
if (isArrayBuffer(arg)) return arg.slice(0); |
|
if (isTypedArray(arg)) return arg.slice(0); |
|
if (isDataView(arg)) { |
|
const { buffer, byteLength, byteOffset } = arg; |
|
return new DataView(buffer.slice(0), byteOffset, byteLength); |
|
} |
|
if (isArray(arg)) return arg.map(deepClone); |
|
if (isPlainObject(arg)) return { ...arg }; |
|
if (isSet(arg)) return new Set([...arg].map(deepClone)); |
|
if (isMap(arg)) { |
|
return new Map([...arg].map(([k, v]) => deepClone([k, v]))); |
|
} |
|
if (isWeakSet(arg)) return new WeakSet(); |
|
if (isWeakMap(arg)) return new WeakMap(); |
|
if (isWeakRef(arg) && arg.deref()) return new WeakRef(arg.deref()!); |
|
if (isError(arg)) { |
|
return new ( |
|
arg.constructor as typeof Error |
|
)(arg.message, { cause: arg.cause }); |
|
} |
|
if (isDate(arg)) return new Date(arg.getTime()); |
|
if (isRegExp(arg)) return new RegExp(arg.source, arg.flags); |
|
return arg; |
|
} |
|
|
|
// ---------------------------------------------------------------------------- |
|
// Core Type class |
|
// ---------------------------------------------------------------------------- |
|
|
|
export interface Type<T = unknown, I = any, O = T> { |
|
(input: I): T; |
|
|
|
readonly constructor: new (...args: any[]) => Type<T>; |
|
} |
|
|
|
export abstract class Type<T = unknown, I = any, O = T> |
|
extends Callable<void, unknown[], Type<T, I, O>> { |
|
#info: TypeInfo<T>; |
|
#ctx: Context<T, I, O> | undefined; |
|
|
|
declare readonly _typeof: T; |
|
|
|
constructor( |
|
readonly name: string = "Type", |
|
info?: Partial<TypeInfoState<T>>, |
|
) { |
|
super((input) => this.as(input)); |
|
|
|
const args = [name, info]; |
|
// drop the name for subclasses by default |
|
if (new.target !== Type) args.shift(); |
|
|
|
this.#info = new TypeInfo({ |
|
...defaultTypeInfoState, |
|
args, |
|
type: this, |
|
name, |
|
...info, |
|
}); |
|
} |
|
|
|
get context(): Context<T, I, O> { |
|
return this.#ctx ??= new Context(); |
|
} |
|
|
|
get info(): TypeInfo<T, this> { |
|
return this.#info as TypeInfo<T, this>; |
|
} |
|
|
|
is(input: unknown): input is T { |
|
const fn = this.info.getValidator(); |
|
let ok = false; |
|
if (isFunction(fn)) ok = FunctionPrototypeCall(fn, this, input); |
|
if (ok) { |
|
const constraints = this.info.getConstraints(); |
|
for (const { predicate } of constraints) { |
|
if (!FunctionPrototypeCall(predicate, this, input)) return false; |
|
} |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
validate(input: unknown, ctx: Context<T, I, O>): ValidationResult<T> { |
|
ctx ??= this.context; |
|
const fn = this.info.getValidator(); |
|
let ok = false; |
|
if (isFunction(fn)) ok = FunctionPrototypeCall(fn, this, input); |
|
if (ok) { |
|
const constraints = this.info.getConstraints(); |
|
for (const { name, predicate, message } of constraints) { |
|
if (!FunctionPrototypeCall(predicate, this, input)) { |
|
let msg = message ?? |
|
`Input failed to satisfy the '${name.toString()}' constraint on type ${this}.`; |
|
msg = msg.replace( |
|
/%(\w+)|\{\{\s*(\w+)\s*\}\}|\{\s*(\w+)\s*\}/g, |
|
(m, a, b, c) => { |
|
const key = a ?? b ?? c; |
|
let val = ReflectGet(this, key) ?? ReflectGet(this.info._, key); |
|
if (key === "type") { |
|
val = this.info.getType(); |
|
} else if (key === "name") { |
|
val = this.info.getName(); |
|
} else if ( |
|
key === "input" || key === "value" || key === "actual" || |
|
key === "s" || key === "i" || key === "o" || key === "v" |
|
) { |
|
val = input; |
|
} else if (key === "path") { |
|
val = ctx.path.length ? ctx.path.join(".") : "<root>"; |
|
} else if (key === "errors" && ctx.errors.length) { |
|
val = " - " + ctx.errors.map((e) => e.error).join(" - "); |
|
} |
|
if (val == null) return m; |
|
return String(val); |
|
}, |
|
); |
|
return ctx.err(msg); |
|
} |
|
} |
|
return ctx.ok(input as T); |
|
} |
|
return ctx.err`Type '${input}' is not assignable to type '${this}'`; |
|
} |
|
|
|
as<U>(input: U): Extract<U, T>; |
|
as(input: unknown): T; |
|
as(input: unknown): T { |
|
this.assert(input); |
|
return input; |
|
} |
|
|
|
assert(input: unknown): asserts input is T { |
|
const res = this.decode(input); |
|
if (!res.success) { |
|
throw new ValidationError(res.error, { path: res.path, type: this }); |
|
} |
|
} |
|
|
|
decode(input: unknown): ValidationResult<T> { |
|
return this.validate(input, this.context); |
|
} |
|
|
|
or<B, O extends Type<B>>(other: O | Type<B>): UnionType<T, B> { |
|
if (!(other instanceof Type)) { |
|
throw new TypeError( |
|
`Expected 'other' to be a Type instance, but received ${typeof other}: ${other}`, |
|
); |
|
} |
|
return new UnionType(this, other); |
|
} |
|
|
|
and<B, O extends Type<B>>(other: O | Type<B>): IntersectionType<T, B> { |
|
if (!(other instanceof Type)) { |
|
throw new TypeError( |
|
`Expected 'other' to be a Type instance, but received ${typeof other}: ${other}`, |
|
); |
|
} |
|
return new IntersectionType(this, other); |
|
} |
|
|
|
not<B, O extends Type<B> = Type<B>>(other: O | Type<B>): ExcludeType<T, B> { |
|
if (!(other instanceof Type)) { |
|
throw new TypeError( |
|
`Expected 'other' to be a Type instance, but received ${typeof other}: ${other}`, |
|
); |
|
} |
|
return new ExcludeType(this, other); |
|
} |
|
|
|
xor<B, O extends Type<B> = Type<B>>(other: O | Type<B>): ExcludeType<T, B> { |
|
if (!(other instanceof Type)) { |
|
throw new TypeError( |
|
`Expected 'other' to be a Type instance, but received ${typeof other}: ${other}`, |
|
); |
|
} |
|
return new ExcludeType(this, other); |
|
} |
|
|
|
optional(): OptionalType<T> { |
|
return new OptionalType(this); |
|
} |
|
|
|
nullable(): NullableType<T> { |
|
return new NullableType(this); |
|
} |
|
|
|
notnull(): NonNullableType<T> { |
|
return new NonNullableType(this); |
|
} |
|
|
|
map<U>(mapper: (input: T) => U): Type<U> { |
|
if (typeof mapper !== "function") { |
|
throw new TypeError( |
|
`Expected a mapper function, but received ${typeof mapper}`, |
|
); |
|
} |
|
return new MappedType(this, mapper); |
|
} |
|
|
|
flatMap<U>(mapper: (input: T) => U | Type<U>): Type<U> { |
|
if (typeof mapper !== "function") { |
|
throw new TypeError( |
|
`Expected a mapper function, but received ${typeof mapper}`, |
|
); |
|
} |
|
return new MappedType(this, (input) => { |
|
const t = mapper.call(this, input); |
|
return t != null && t instanceof Type ? t.as(input) : t; |
|
}); |
|
} |
|
|
|
filter(predicate: (input: T) => boolean): Type<T> { |
|
if (typeof predicate !== "function") { |
|
throw new TypeError( |
|
`Expected a predicate function, but received ${typeof predicate}`, |
|
); |
|
} |
|
return new FilterType(this, predicate); |
|
} |
|
|
|
transmute<U>(transmuter: (input: T) => U): Type<U> { |
|
if (typeof transmuter !== "function") { |
|
throw new TypeError( |
|
`Expected a transmuter function, but received ${typeof transmuter}`, |
|
); |
|
} |
|
return new TransmuteType(this, transmuter); |
|
} |
|
|
|
cast<U>(caster: (input: T) => U): Type<U> { |
|
if (typeof caster !== "function") { |
|
throw new TypeError( |
|
`Expected a caster function, but received ${typeof caster}`, |
|
); |
|
} |
|
return new CastType(this, caster); |
|
} |
|
|
|
refine<U extends T>( |
|
refiner: (it: T) => it is U, |
|
message?: string, |
|
): Type<U>; |
|
refine<U extends T>( |
|
refiner: (it: T) => unknown, |
|
message?: string, |
|
): Type<U>; |
|
refine<U extends T>( |
|
refiner: (it: T) => it is U, |
|
message?: string, |
|
): Type<U> { |
|
if (typeof refiner !== "function") { |
|
throw new TypeError( |
|
`Expected a refiner function, but received ${typeof refiner}`, |
|
); |
|
} |
|
const o = { [_type]: this, type: this }; |
|
const p = { ...o }; |
|
return RefineType.from(this, refiner, { message }); |
|
} |
|
|
|
brand<U, K extends PropertyKey>( |
|
key: K, |
|
value: U, |
|
): Type<T & { [P in K]: U }> { |
|
return new BrandType(this, key, value); |
|
} |
|
|
|
clone(): this { |
|
const info = { |
|
...this.info.state, |
|
type: this, |
|
constraints: [...this.info.getConstraints()].map((c) => ({ ...c })), |
|
}; |
|
|
|
let args = [...this.#info.state.args]; |
|
if (args.length) args = deepClone(args); |
|
|
|
const clone = new (this.constructor as any)(...args); |
|
info.args = args as any; |
|
clone.#info._ = info; |
|
return clone; |
|
} |
|
|
|
addConstraint<K extends PropertyKey, V = any>( |
|
predicate: (value: T) => boolean, |
|
key: K, |
|
message = `Expected ${this.name} to satisfy ${key.toString()}`, |
|
): this & Type<T & { [P in K]: V }> { |
|
this.info.constraint(key, predicate, message); |
|
return this as never; |
|
} |
|
|
|
withConstraint<K extends PropertyKey, V = any>( |
|
predicate: (value: T) => boolean, |
|
key: K, |
|
message?: string, |
|
): this & Type<T & { [P in K]: V }> { |
|
return this.clone().addConstraint(predicate, key, message); |
|
} |
|
|
|
/** |
|
* Creates a new instance of this type using the provided arguments. The |
|
* constructor of the type is called with the provided arguments, and the |
|
* resulting instance is returned. |
|
* |
|
* This is useful for refining certain types with additional configurations, |
|
* especially when used on the built-in instances provided by this library |
|
* like `string`, `number`, `boolean`, etc. |
|
* |
|
* @example |
|
* ```ts |
|
* import * as t from "@nick/lint-core/runtime-types"; |
|
* |
|
* const str = t.string.with({ minLength: 5 }); |
|
* ``` |
|
* @example |
|
* ```ts |
|
* import * as t from "@nick/lint-core/runtime-types"; |
|
* |
|
* const map1 = t.map.with(t.string, t.unknown); |
|
* |
|
* // the following is equivalent to the above call, for convenience: |
|
* const map2 = t.map(t.string, t.unknown); |
|
* ``` |
|
*/ |
|
with<U extends T>( |
|
...args: ConstructorParameters<this["constructor"]> |
|
): Type<U> { |
|
const Ctor = this.constructor as any as new (...a: typeof args) => Type<U>; |
|
return new Ctor(...args); |
|
} |
|
|
|
[inspect.custom](depth: number | null, options: InspectOptionsStylized) { |
|
const s = options?.stylize ?? stylize; |
|
let type = this.toString(); |
|
// if (this.info.hasType()) type = this.info.getType().toString(); |
|
if (depth == null || depth < 1) type = `[${type}]`; |
|
if (!getNoColor()) type = s(type, "special"); |
|
return type; |
|
} |
|
|
|
toJSON(): unknown { |
|
return this.name; |
|
} |
|
|
|
override toString(): string { |
|
return this.name; |
|
} |
|
|
|
/** |
|
* Utility for rendering an arbitrary value in a string format suitable for |
|
* text-only contexts like a terminal emulator or browser console. This is |
|
* a wrapper around the `inspect` function from `node:util`, with some custom |
|
* defaults that align better with the specific needs of our use case. |
|
* |
|
* @param value The value to inspect. |
|
* @param [options] An optional options object for customizing the output of |
|
* the inspect process. By default, colors are disabled, objects with 3 props |
|
* or less are printed in compact mode, getters are rendered, and |
|
*/ |
|
static inspect(value: unknown, options?: InspectOptionsStylized): string { |
|
return inspect(value, { |
|
colors: false, |
|
compact: 3, |
|
getters: "get", |
|
customInspect: true, |
|
sorted: true, |
|
depth: 8, |
|
...options, |
|
}); |
|
} |
|
|
|
static [inspect.custom]( |
|
_depth: number | null, |
|
options: InspectOptionsStylized, |
|
): string { |
|
if (!Object.prototype.isPrototypeOf.call(this, Type)) return this.name; |
|
let s = options?.stylize ?? stylize; |
|
if (getNoColor()) s = identity; |
|
return s( |
|
`[Type${this.name === "Type" ? "" : `<${this.name}>`}]`, |
|
"special", |
|
); |
|
} |
|
|
|
static { |
|
Object.assign(this, { defaultRenderOptions, defaultInspectOptions }); |
|
} |
|
} |
|
|
|
export declare namespace Type { |
|
export type of<T, F = never> = Type.type<T, F>; |
|
export type type<T, F = never> = T extends Type<infer U, any, any> ? U : F; |
|
export type input<T, F = never> = T extends Type<any, infer I, any> ? I : F; |
|
export type output<T, F = never> = T extends Type<any, any, infer O> ? O : F; |
|
} |
|
|
|
export interface AnyType extends Type<any, any, any> {} |
|
export interface UnknownType extends Type<unknown, any, unknown> {} |
|
|
|
// ---------------------------------------------------------------------------- |
|
// Primitive Types |
|
// ---------------------------------------------------------------------------- |
|
|
|
type PrimitiveTypeMap = { |
|
string: string; |
|
number: number; |
|
boolean: boolean; |
|
bigint: bigint; |
|
symbol: symbol; |
|
undefined: undefined; |
|
null: null; |
|
}; |
|
|
|
export type Primitive = |
|
| string |
|
| number |
|
| bigint |
|
| boolean |
|
| symbol |
|
| null |
|
| undefined |
|
| void; |
|
|
|
export class PrimitiveType<T extends Primitive = Primitive> extends Type<T> { |
|
static override readonly name: "Primitive" | strings = "Primitive"; |
|
|
|
override name: "Primitive" | strings; |
|
|
|
constructor(name: string, info?: Partial<TypeInfoState<T>>); |
|
constructor(info?: Partial<TypeInfoState<T>>); |
|
constructor(...args: unknown[]); |
|
constructor( |
|
name?: string | Partial<TypeInfoState<T>>, |
|
info?: Partial<TypeInfoState<T>>, |
|
) { |
|
name ??= PrimitiveType.name; |
|
if (!isString(name)) { |
|
info = name; |
|
name = String(info.name ?? PrimitiveType.name); |
|
} |
|
const validator = (input: unknown): input is T => { |
|
if (this.name === PrimitiveType.name) { |
|
return isPrimitive(input); |
|
} else if (this.name === "null") { |
|
return isNull(input); |
|
} else { |
|
// deno-lint-ignore valid-typeof |
|
return this.name === typeof input; |
|
} |
|
}; |
|
info ??= { name, validator }; |
|
if (new.target === PrimitiveType) { |
|
info.validator = validator; |
|
info.name = name = PrimitiveType.name; |
|
info.args = [name, info]; |
|
} else { |
|
info.args = [info]; |
|
} |
|
super(name as string, info); |
|
this.name = name as string; |
|
} |
|
|
|
validator(input: unknown): input is T { |
|
const fn = this.info.getValidator(); |
|
if (!fn(input)) return false; |
|
const constraints = this.info.getConstraints(); |
|
return [...constraints].every(({ predicate }) => predicate(input)); |
|
} |
|
|
|
override as(input: unknown): T { |
|
if (this.is(input)) return input; |
|
throw new TypeError( |
|
`Expected ${this.name}, got ${input === null ? "null" : typeof input}`, |
|
); |
|
} |
|
|
|
override validate(input: unknown, ctx: Context<T>): ValidationResult<T> { |
|
if (this.is(input)) return ctx.ok(input); |
|
for (const { name, predicate, message } of this.info.getConstraints()) { |
|
if (!predicate(input)) { |
|
ctx.add( |
|
message ?? |
|
`Input failed to satisfy the '${name.toString()}' constraint.`, |
|
); |
|
} |
|
} |
|
return ctx.err`Type ${input} is not assignable to type ${this}`; |
|
} |
|
} |
|
|
|
export const primitive: PrimitiveType<Primitive> = new PrimitiveType(); |
|
|
|
const _string_length_gt: unique symbol = Symbol( |
|
"string minimum length (exclusive)", |
|
); |
|
const _string_length_lt: unique symbol = Symbol( |
|
"string maximum length (exclusive)", |
|
); |
|
const _string_length_gte: unique symbol = Symbol( |
|
"string minimum length (inclusive)", |
|
); |
|
const _string_length_lte: unique symbol = Symbol( |
|
"string maximum length (inclusive)", |
|
); |
|
const _string_length_eq: unique symbol = Symbol("string length equals"); |
|
const _string_matches: unique symbol = Symbol("string matches"); |
|
const _string_matches_strict: unique symbol = Symbol("string matches strict"); |
|
const _string_not_matches: unique symbol = Symbol("string does not match"); |
|
const _string_is_email: unique symbol = Symbol("string is email"); |
|
const _string_is_url: unique symbol = Symbol("string is url"); |
|
const _string_is_uuid: unique symbol = Symbol("string is uuid"); |
|
const _string_is_date: unique symbol = Symbol("string is date"); |
|
const _string_is_hex: unique symbol = Symbol("string is hex"); |
|
const _string_is_base64: unique symbol = Symbol("string is base64"); |
|
const _string_is_json: unique symbol = Symbol("string is json"); |
|
const _string_is_utf8: unique symbol = Symbol("string is utf8"); |
|
|
|
export interface StringTypeInfoState<T extends string = string> |
|
extends TypeInfoState<T, StringType<T>> { |
|
constraints: TypeConstraint<T>[]; |
|
} |
|
|
|
export interface StringType<T extends string = string> { |
|
readonly constructor: typeof StringType<T>; |
|
} |
|
|
|
export class StringType<T extends string = string> extends PrimitiveType<T> { |
|
static override readonly name = "string"; |
|
|
|
constructor(info?: Partial<StringTypeInfoState<T>>) { |
|
super("string", info); |
|
} |
|
|
|
/** |
|
* Validates a string against a minimum length constraint (inclusive). |
|
* |
|
* @param min The minimum length. |
|
* @param inclusive Whether or not the length constraint should be inclusive. |
|
* If `true`, the comparison is performed with the `>=` operator; otherwise, |
|
* it will be performed using the `>` operator. Defaults to `false`. |
|
* @returns A copy of this `StringType` instance with an additional constraint |
|
* requiring the string length to be greater than or equal to `min`. |
|
*/ |
|
min<N extends number>( |
|
min: N, |
|
inclusive: true, |
|
): StringType<T & { "len >=": N }>; |
|
/** |
|
* Validates a string against a minimum length constraint (exclusive). |
|
* |
|
* @param min The minimum length. |
|
* @param [inclusive=false] Controls the exclusivity of the constraint. If |
|
* `true`, the comparison is performed with the `>=` operator; otherwise, it |
|
* will be performed with the `>` operator. Defaults to `false`. |
|
*/ |
|
min<N extends number>( |
|
min: N, |
|
inclusive?: false, |
|
): StringType<T & { "len >": N }>; |
|
|
|
/** @internal */ |
|
min<N extends number>( |
|
min: N, |
|
inclusive: boolean, |
|
): StringType<T & ({ "len >": N } | { "len >=": N })>; |
|
|
|
/** @internal */ |
|
min(min: number, inc?: boolean): AnyType { |
|
return this.withConstraint( |
|
(s) => (inc && s.length === min) || s.length > min, |
|
`len ${inc ? ">=" : ">"}`, |
|
`Expected string length to be ${inc ? ">=" : ">"} ${min}`, |
|
); |
|
} |
|
|
|
/** |
|
* Adds a constraint to the string type requiring its length be less than |
|
* or equal to the provided `max` value. If `inclusive` is `true`, the check |
|
* uses the inclusive `<=` operator; otherwise, the exclusive `<` is used to |
|
* perform the comparison. |
|
* |
|
* @param max The maximum length. |
|
* @param inclusive Controls the inclusivity of the constraint. |
|
* @returns A new {@linkcode StringType} with the maximum length constraint. |
|
*/ |
|
max<N extends number>( |
|
max: N, |
|
inclusive: true, |
|
): StringType<T & { "len <=": N }>; |
|
|
|
/** |
|
* Adds a constraint to the string type requiring its length be less than |
|
* or equal to the provided `max` value. If `inclusive` is `true` the check |
|
* uses the inclusive `<=` operator; otherwise, the exclusive `<` is used to |
|
* perform the comparison. |
|
* |
|
* @param max The maximum length. |
|
* @param [inclusive=false] Controls the exclusivity of the constraint. |
|
* @returns A new {@linkcode StringType} with the maximum length constraint. |
|
*/ |
|
max<N extends number>( |
|
max: N, |
|
inclusive?: false, |
|
): StringType<T & { "len <": N }>; |
|
|
|
/** @internal */ |
|
max<N extends number>( |
|
max: N, |
|
inclusive: boolean, |
|
): StringType<T & ({ "len <": N } | { "len <=": N })>; |
|
|
|
/** @internal */ |
|
max(max: number, inc?: boolean): AnyType { |
|
return this.withConstraint( |
|
(s) => (inc && s.length === max) || s.length < max, |
|
`len ${inc ? "<=" : "<"}`, |
|
`Expected string length to be ${inc ? "<=" : "<"} ${max}`, |
|
); |
|
} |
|
|
|
/** |
|
* Validates a string with a specific length. |
|
* |
|
* @param length The exact length. |
|
* @returns A new {@linkcode StringType} with the exact length constraint. |
|
*/ |
|
len<N extends number>( |
|
length: N, |
|
): StringType<T & { "len =": N }>; |
|
|
|
/** |
|
* Validates a string with a specific length. |
|
* |
|
* @param length The exact length. |
|
* @returns A new {@linkcode StringType} with the exact length constraint. |
|
*/ |
|
len(length: number): StringType<T & { "len =": number }>; |
|
|
|
/** @internal */ |
|
len(length: number): AnyType { |
|
return this.withConstraint( |
|
(s) => s.length === length, |
|
"len =", |
|
`Expected string length to be ${length}`, |
|
); |
|
} |
|
|
|
/** |
|
* Validates a string against a specific RegExp pattern or string. |
|
* |
|
* @param pattern The regular expression pattern, or a string to |
|
* create a new RegExp instance from. |
|
* @returns A new {@linkcode StringType} with a pattern constraint, which |
|
* further validates strings, ensuring they satisfy the constraint's pattern. |
|
*/ |
|
match<P extends string | RegExp>( |
|
pattern: P, |
|
message?: string, |
|
): StringType< |
|
T & { |
|
" matches$": P; |
|
} |
|
>; |
|
/** |
|
* Validates a string with a specific pattern. |
|
* |
|
* @param pattern The regular expression pattern. |
|
* @returns A new {@linkcode StringType} with the pattern constraint. |
|
*/ |
|
match(pattern: string | RegExp, message?: string): StringType< |
|
T & { |
|
" matches$": string | RegExp; |
|
} |
|
>; |
|
/** @internal */ |
|
match(pattern: string | RegExp, message?: string): AnyType { |
|
return this.withConstraint( |
|
// we construct a new RegExp instance on each call to avoid the footgun |
|
// issues surrounding the lastIndex property. otherwise we would have to |
|
// make sure we reset pattern.lastIndex = 0 before every .test call. |
|
(s): s is T => new RegExp(pattern).test(s), |
|
"matches pattern", |
|
message ?? `Expected the string to match the pattern: ${pattern}`, |
|
); |
|
} |
|
|
|
/** |
|
* Validates a string that is a valid email address. |
|
* |
|
* @returns A new {@linkcode StringType} with the email constraint. |
|
*/ |
|
email(negate: true): StringType<T & { "is email": false }>; |
|
email(negate: false): StringType<T & { "is email": true }>; |
|
email(negate?: boolean): StringType<T & { "is email": boolean }>; |
|
/** @internal */ |
|
email(negate?: boolean): AnyType { |
|
const pat = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; |
|
return this.withConstraint( |
|
(s) => { |
|
pat.lastIndex = 0; |
|
return pat.test(s) === !negate; |
|
}, |
|
"is email", |
|
"Expected a valid email address (e.g. ross@ulbricht.free)", |
|
); |
|
} |
|
|
|
/** |
|
* Validates a string that is a valid URL. |
|
* |
|
* @returns A new {@linkcode StringType} with the URL constraint. |
|
*/ |
|
url(): StringType<T & { "is url": true }>; |
|
url(negate: true): StringType<T & { "is url": false }>; |
|
url(negate?: boolean): StringType<T & { "is url": boolean }>; |
|
/** @internal */ |
|
url(negate?: boolean): AnyType { |
|
return this.withConstraint( |
|
(s): s is any => isURLString(s) === !negate, |
|
" is url", |
|
"Expected a valid URL string (e.g. https://github.com/nberlette)", |
|
); |
|
} |
|
|
|
/** |
|
* Validates a string that is a valid UUID. |
|
* |
|
* @returns A new {@linkcode StringType} with the UUID constraint. |
|
*/ |
|
uuid(): StringType<T & { "is uuid": true }>; |
|
uuid(negate: true): StringType<T & { "is uuid": false }>; |
|
uuid(negate?: boolean): StringType<T & { "is uuid": boolean }>; |
|
/** @internal */ |
|
uuid(negate?: boolean): AnyType { |
|
return this.withConstraint( |
|
(s): s is any => |
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test( |
|
s, |
|
) === !negate, |
|
"is uuid", |
|
"Expected a valid UUID string (e.g. 550e8400-e29b-41d4-a716-446655440000)", |
|
); |
|
} |
|
|
|
/** |
|
* Validates a string that is a valid ISO date. |
|
* |
|
* @returns A new {@linkcode StringType} with the ISO date constraint. |
|
*/ |
|
date(): StringType<T & { "is date": true }>; |
|
date(negate: true): StringType<T & { "is date": false }>; |
|
date(negate?: boolean): StringType<T & { "is date": boolean }>; |
|
/** @internal */ |
|
date(negate?: boolean): AnyType { |
|
return this.withConstraint( |
|
(s) => isDateString(s) === !negate, |
|
"is date", |
|
"Expected a valid date string (e.g. 2021-01-06)", |
|
); |
|
} |
|
|
|
override refine<U extends T>( |
|
predicate: (value: T) => value is U, |
|
message?: string, |
|
): StringType<U>; |
|
|
|
override refine( |
|
predicate: (value: T) => boolean, |
|
message?: string, |
|
): StringType<T>; |
|
|
|
override refine<U extends T>( |
|
predicate: (value: T) => value is U, |
|
message?: string, |
|
): StringType<U> { |
|
return ObjectSetPrototypeOf( |
|
super.refine(predicate, message), |
|
StringType.prototype, |
|
); |
|
} |
|
} |
|
|
|
export const string: StringType = new StringType(); |
|
|
|
export interface NumberType<N extends number = number> { |
|
readonly constructor: typeof NumberType<N>; |
|
} |
|
|
|
export class NumberType<N extends number = number> extends PrimitiveType<N> { |
|
static override readonly name = "number"; |
|
|
|
constructor(info?: Partial<TypeInfoState<N, NumberType<N>>>) { |
|
super("number", info); |
|
} |
|
|
|
/** |
|
* Validates a number against a minimum value constraint (inclusive). |
|
* |
|
* @param min The minimum value. |
|
* @param inclusive Whether or not the value constraint should be inclusive. |
|
* If `true`, the comparison is performed with the `>=` operator; otherwise, |
|
* it will be performed using the `>` operator. Defaults to `false`. |
|
*/ |
|
min<M extends number>( |
|
min: M, |
|
inclusive: true, |
|
): NumberType<N & { ">=": M }>; |
|
|
|
/** |
|
* Validates a number against a minimum value constraint (exclusive). |
|
* |
|
* @param min The minimum value. |
|
* @param [inclusive=false] Controls the exclusivity of the constraint. If |
|
* `true`, the comparison is performed with the `>=` operator; otherwise, it |
|
* will be performed using the `>` operator. Defaults to `false`. |
|
*/ |
|
min<M extends number>( |
|
min: M, |
|
inclusive?: false, |
|
): NumberType<N & { ">": M }>; |
|
|
|
/** @internal */ |
|
min<M extends number>( |
|
min: M, |
|
inclusive: boolean, |
|
): NumberType<N & ({ ">": M } | { ">=": M })>; |
|
|
|
/** @internal */ |
|
min(min: number, inc?: boolean): AnyType { |
|
return this.withConstraint( |
|
(n) => (inc && n >= min) || n > min, |
|
inc ? ">=" : ">", |
|
`Expected number to be ${inc ? ">=" : ">"} ${min}`, |
|
); |
|
} |
|
|
|
/** |
|
* Validates a number against a maximum value constraint (inclusive). |
|
* |
|
* @param max The maximum value. |
|
* @param inclusive Whether or not the value constraint should be inclusive. |
|
* If `true`, the comparison is performed with the `<=` operator; otherwise, |
|
* it will be performed using the `<` operator. Defaults to `false`. |
|
*/ |
|
max<M extends number>( |
|
max: M, |
|
inclusive: true, |
|
): NumberType<N & { "<=": M }>; |
|
|
|
/** |
|
* Validates a number against a maximum value constraint (exclusive). |
|
* |
|
* @param max The maximum value. |
|
* @param [inclusive=false] Controls the exclusivity of the constraint. If |
|
* `true`, the comparison is performed with the `<=` operator; otherwise, it |
|
* will be performed using the `<` operator. Defaults to `false`. |
|
*/ |
|
max<M extends number>( |
|
max: M, |
|
inclusive?: false, |
|
): NumberType<N & { "<": M }>; |
|
|
|
/** @internal */ |
|
max<M extends number>( |
|
max: M, |
|
inclusive: boolean, |
|
): NumberType<N & ({ "<": M } | { "<=": M })>; |
|
|
|
/** @internal */ |
|
max(max: number, inc?: boolean): AnyType { |
|
return this.withConstraint( |
|
(n) => (inc && n <= max) || n < max, |
|
inc ? "<=" : "<", |
|
`Expected number to be ${inc ? "<=" : "<"} ${max}`, |
|
); |
|
} |
|
|
|
/** |
|
* Creates a new `NumberType` from the current instance, adding a constraint |
|
* that ensures its value is equal to the provided `value`. |
|
* |
|
* @param value The exact value. |
|
* @returns A new {@linkcode NumberType} with the exact value constraint. |
|
*/ |
|
eq<M extends number>( |
|
value: M, |
|
): NumberType<M & { "===": M }>; |
|
|
|
/** |
|
* Creates a new `NumberType` from the current instance, adding a constraint |
|
* that ensures its value is equal to the provided `value`. |
|
* |
|
* @param value The exact value. |
|
* @returns A new {@linkcode NumberType} with the exact value constraint. |
|
*/ |
|
eq(value: number): NumberType<N & { "===": number }>; |
|
|
|
/** @internal */ |
|
eq(value: number): AnyType { |
|
return this.withConstraint( |
|
(n) => n === value, |
|
"===", |
|
`Expected number to be ${value}`, |
|
); |
|
} |
|
|
|
/** |
|
* Creates a new `NumberType` from the current instance, adding a constraint |
|
* that ensures its value is a valid integer. |
|
* |
|
* @returns A new {@linkcode NumberType} with the integer constraint. |
|
*/ |
|
integer(): NumberType<N & { "is integer": true }>; |
|
/** @internal */ |
|
integer(): AnyType { |
|
return this.withConstraint( |
|
(n) => isInteger(n), |
|
"is integer", |
|
"Expected number to be an integer", |
|
); |
|
} |
|
|
|
/** |
|
* Creates a new `NumberType` from the current instance, adding a constraint |
|
* that ensures its value is a valid floating point number (float). |
|
* |
|
* @returns A new {@linkcode NumberType} with the float constraint. |
|
*/ |
|
float(): NumberType<N & { "is float": true }>; |
|
/** @internal */ |
|
float(): AnyType { |
|
return this.withConstraint( |
|
(n) => isFloat(n), |
|
"is float", |
|
"Expected number to be a float", |
|
); |
|
} |
|
|
|
/** |
|
* Creates a new `NumberType` from the current instance, adding a constraint |
|
* that ensures its value is a valid finite number. |
|
* |
|
* @returns A new {@linkcode NumberType} with the finite constraint. |
|
*/ |
|
finite(): NumberType<N & { "is finite": true }>; |
|
/** @internal */ |
|
finite(): AnyType { |
|
return this.withConstraint( |
|
(n) => isFinite(n), |
|
"is finite", |
|
"Expected number to be finite", |
|
); |
|
} |
|
|
|
/** |
|
* Creates a new `NumberType` from the current instance, adding a constraint |
|
* that ensures its value is a valid safe integer. |
|
* |
|
* @returns A new {@linkcode NumberType} with the safe integer constraint. |
|
*/ |
|
safeInteger(): NumberType<N & { "is safe-integer": true }>; |
|
/** @internal */ |
|
safeInteger(): AnyType { |
|
return this.withConstraint( |
|
(n) => Number.isSafeInteger(n), |
|
"is safe-integer", |
|
"Expected number to be a safe integer", |
|
); |
|
} |
|
|
|
/** |
|
* Creates a new `NumberType` from the current instance, adding a constraint |
|
* that ensures its value is a valid positive number. |
|
* |
|
* @returns A new {@linkcode NumberType} with the positive constraint. |
|
*/ |
|
positive(): NumberType<N & { "is positive": true }>; |
|
/** @internal */ |
|
positive(): AnyType { |
|
return this.withConstraint( |
|
(n) => isPositive(n), |
|
"is positive", |
|
"Expected number to be positive", |
|
); |
|
} |
|
|
|
/** |
|
* Creates a new `NumberType` from the current instance, adding a constraint |
|
* that ensures its value is a valid negative number. |
|
* |
|
* @returns A new {@linkcode NumberType} with the negative constraint. |
|
*/ |
|
negative(): NumberType<N & { "is negative": true }>; |
|
/** @internal */ |
|
negative(): AnyType { |
|
return this.withConstraint( |
|
(n) => isNegative(n), |
|
"is negative", |
|
"Expected number to be negative", |
|
); |
|
} |
|
|
|
/** |
|
* Creates a new `NumberType` from the current instance, adding a constraint |
|
* that ensures its value is a valid non-zero number. |
|
* |
|
* @returns A new {@linkcode NumberType} with the non-zero constraint. |
|
*/ |
|
nonzero(): NumberType<N & { "not zero": true }>; |
|
/** @internal */ |
|
nonzero(): AnyType { |
|
return this.withConstraint( |
|
(n) => isNonZero(n), |
|
"not zero", |
|
"Expected a non-zero number", |
|
); |
|
} |
|
} |
|
|
|
export const number: NumberType = new NumberType(); |
|
|
|
export interface BooleanType<B extends boolean = boolean> { |
|
readonly constructor: typeof BooleanType<B>; |
|
} |
|
|
|
export class BooleanType<B extends boolean = boolean> extends PrimitiveType<B> { |
|
static override readonly name = "boolean"; |
|
|
|
constructor(info?: Partial<TypeInfoState<B, BooleanType<B>>>) { |
|
super("boolean", info); |
|
} |
|
} |
|
|
|
export const boolean: BooleanType = new BooleanType(); |
|
|
|
export interface BigIntType<I extends bigint = bigint> { |
|
readonly constructor: typeof BigIntType<I>; |
|
} |
|
|
|
export class BigIntType<I extends bigint = bigint> extends PrimitiveType<I> { |
|
static override readonly name = "bigint"; |
|
|
|
constructor(info?: Partial<TypeInfoState<I, BigIntType<I>>>) { |
|
super("bigint", info); |
|
} |
|
} |
|
|
|
export const bigint: BigIntType = new BigIntType(); |
|
|
|
export interface SymbolType<S extends symbol = symbol> { |
|
readonly constructor: typeof SymbolType<S>; |
|
} |
|
|
|
export class SymbolType<S extends symbol = symbol> extends PrimitiveType<S> { |
|
static override readonly name = "symbol"; |
|
|
|
constructor(info?: Partial<TypeInfoState<S, SymbolType<S>>>) { |
|
super("symbol", info); |
|
} |
|
|
|
wellknown(): WellKnownSymbolType { |
|
return this.wellKnown(); |
|
} |
|
|
|
@alias("wellknown") |
|
wellKnown(): WellKnownSymbolType { |
|
return new WellKnownSymbolType(); |
|
} |
|
|
|
registered(): RegisteredSymbolType { |
|
return new RegisteredSymbolType(); |
|
} |
|
|
|
unique(): UniqueSymbolType { |
|
return new UniqueSymbolType(); |
|
} |
|
} |
|
|
|
export const symbol: SymbolType = new SymbolType(); |
|
|
|
export interface UndefinedType { |
|
readonly constructor: typeof UndefinedType; |
|
} |
|
|
|
export class UndefinedType extends PrimitiveType<undefined> { |
|
static override readonly name = "undefined"; |
|
|
|
constructor(info?: Partial<TypeInfoState<undefined, UndefinedType>>) { |
|
super("undefined", { |
|
...info, |
|
validator: (x: unknown): x is undefined => typeof x === "undefined", |
|
}); |
|
} |
|
} |
|
|
|
const _undefined: UndefinedType = new UndefinedType(); |
|
export { _undefined as undefined }; |
|
|
|
export interface VoidType { |
|
readonly constructor: typeof VoidType; |
|
} |
|
|
|
export class VoidType extends PrimitiveType<void> { |
|
constructor(info?: Partial<TypeInfoState<void>>) { |
|
super( |
|
"void", |
|
{ |
|
...info, |
|
validator: (x: unknown): x is void => typeof x === "undefined", |
|
}, |
|
); |
|
} |
|
} |
|
|
|
const _void: VoidType = new VoidType(); |
|
export { _void as void }; |
|
|
|
export interface NullType { |
|
readonly constructor: typeof NullType; |
|
} |
|
|
|
export class NullType extends Type<null> { |
|
static override readonly name = "null"; |
|
|
|
constructor(info?: Partial<TypeInfoState<null>>) { |
|
super("null", info); |
|
} |
|
|
|
override is(input: unknown): input is null { |
|
return input === null; |
|
} |
|
} |
|
|
|
const _null: NullType = new NullType(); |
|
export { _null as null }; |
|
|
|
export interface Any { |
|
readonly constructor: typeof Any; |
|
} |
|
|
|
export class Any extends Type<any, any, any> { |
|
static override readonly name = "any"; |
|
|
|
constructor(info?: Partial<TypeInfoState<any>>) { |
|
super("any", { ...info, validator: (_): _ is any => true }); |
|
} |
|
|
|
override as(input: unknown): any { |
|
return input; |
|
} |
|
|
|
override validate( |
|
input: unknown, |
|
ctx: Context<any, any, any>, |
|
): ValidationResult<any> { |
|
return ctx.ok(input); |
|
} |
|
} |
|
export const any: Any = new Any(); |
|
|
|
// @ts-expect-error readonly property reassignment |
|
defaultTypeInfoState.type = any; |
|
any.info.type(any); |
|
|
|
export interface Unknown { |
|
readonly constructor: typeof Unknown; |
|
} |
|
|
|
export class Unknown extends Type<unknown, any, unknown> { |
|
static override readonly name = "unknown"; |
|
|
|
constructor(info?: Partial<TypeInfoState<unknown, Unknown>>) { |
|
super("unknown", { ...info, validator: (_): _ is unknown => true }); |
|
} |
|
|
|
override as(input: unknown): unknown { |
|
return input; |
|
} |
|
|
|
override validate( |
|
input: any, |
|
ctx: Context<unknown, any, unknown>, |
|
): ValidationResult<unknown> { |
|
return ctx.ok(input); |
|
} |
|
} |
|
export const unknown: Unknown = new Unknown(); |
|
|
|
export interface Never { |
|
readonly constructor: typeof Never; |
|
} |
|
|
|
export class Never extends Type<never, any, never> { |
|
static override readonly name = "never"; |
|
|
|
constructor(info?: Partial<TypeInfoState<never, Never>>) { |
|
super("never", { ...info, validator: (_): _ is never => false }); |
|
} |
|
|
|
override as(input: unknown): never { |
|
throw new TypeError(`Type '${input}' is not assignable to type 'never'.`); |
|
} |
|
|
|
override validate( |
|
input: unknown, |
|
ctx: Context<never, any, never>, |
|
): ValidationResult<never> { |
|
void input; |
|
return ctx.err`Type '${input}' is not assignable to type 'never'.`; |
|
} |
|
} |
|
|
|
export const never: Never = new Never(); |
|
|
|
export interface ExcludeType<T, U> { |
|
readonly constructor: typeof ExcludeType<T, U>; |
|
} |
|
|
|
export class ExcludeType<T, U> extends Type<T> { |
|
static override readonly name = "Exclude<T, U>"; |
|
|
|
constructor( |
|
protected base: Type<T>, |
|
protected other: Type<U>, |
|
info?: Partial<TypeInfoState<T>>, |
|
) { |
|
super(`Exclude<${base}, ${other}>`, info); |
|
} |
|
|
|
override is<B>(input: B): input is Exclude<Extract<B, T>, U> { |
|
return this.base.is(input) && !this.other.is(input); |
|
} |
|
|
|
override as<B>(input: B): Exclude<Extract<B, T>, U> { |
|
const t = this.base.as(input); |
|
if (!this.other.is(t)) return t as Exclude<Extract<B, T>, U>; |
|
throw new TypeError( |
|
this.context |
|
.err`Type '${input}' is not assignable to type '${this}'. Reason: types assignable to '${this.other}' are excluded.` |
|
.error, |
|
); |
|
} |
|
|
|
override validate(input: unknown, ctx: Context): ValidationResult<T> { |
|
const res = this.base.validate(input, ctx); |
|
if (res.success) { |
|
if (this.other.is(res.value)) { |
|
return ctx.err`Type '${input}' is not assignable to type '${this}'`; |
|
} |
|
return res; |
|
} |
|
return res; |
|
} |
|
} |
|
|
|
export interface MappedType<T, U> { |
|
readonly constructor: typeof MappedType<T, U>; |
|
} |
|
|
|
export class MappedType<T, U> extends Type<U> { |
|
static override readonly name = "Mapped<T, U>"; |
|
|
|
constructor(protected inner: Type<T>, protected mapper: (input: T) => U) { |
|
super(`Mapped<${inner}, ${mapper}>`); |
|
} |
|
|
|
override as(input: unknown): U { |
|
const t = this.inner.as(input); |
|
return this.mapper(t); |
|
} |
|
|
|
override validate(input: unknown, ctx: Context): ValidationResult<U> { |
|
const res = this.inner.validate(input, ctx); |
|
if (res.success) { |
|
try { |
|
return ctx.ok(this.mapper(res.value)); |
|
} catch (e) { |
|
return ctx.err`MappedType validation failed with error ${e}`; |
|
} |
|
} |
|
return res as ValidationResult<U>; |
|
} |
|
} |
|
|
|
export interface FilterType<T> { |
|
readonly constructor: typeof FilterType<T>; |
|
} |
|
|
|
export class FilterType<T> extends Type<T> { |
|
static override readonly name = "Filter<T>"; |
|
|
|
constructor( |
|
protected inner: Type<T>, |
|
protected predicate: (input: T) => boolean, |
|
) { |
|
super(`Filter<${inner}, ${predicate}>`); |
|
} |
|
|
|
override is(input: unknown): input is T { |
|
try { |
|
const t = this.inner.as(input); |
|
return this.predicate(t); |
|
} catch { |
|
return false; |
|
} |
|
} |
|
|
|
override as(input: unknown): T { |
|
const t = this.inner.as(input); |
|
if (this.predicate(t)) return t; |
|
throw new TypeError(`Filter predicate failed on ${t}`); |
|
} |
|
|
|
override validate(input: unknown, ctx: Context): ValidationResult<T> { |
|
const res = this.inner.validate(input, ctx); |
|
if (res.success) { |
|
if (this.predicate(res.value)) return res; |
|
return ctx.err`Filter predicate failed on ${res.value}`; |
|
} |
|
return res; |
|
} |
|
} |
|
|
|
export interface TransmuteType<T, U> { |
|
readonly constructor: typeof TransmuteType<T, U>; |
|
} |
|
|
|
export class TransmuteType<T, U> extends Type<U> { |
|
static override readonly name = "Transmute<T, U>"; |
|
constructor( |
|
protected inner: Type<T>, |
|
protected transformer: (input: T) => U, |
|
) { |
|
super(`Transmute<${inner}, ${transformer}>`); |
|
} |
|
|
|
override is(input: unknown): input is U { |
|
try { |
|
const t = this.inner.as(input); |
|
this.transformer(t); |
|
return true; |
|
} catch { |
|
return false; |
|
} |
|
} |
|
|
|
override as(input: unknown): U { |
|
const t = this.inner.as(input); |
|
return this.transformer(t); |
|
} |
|
|
|
override validate(input: unknown, ctx: Context): ValidationResult<U> { |
|
const res = this.inner.validate(input, ctx); |
|
if (res.success) { |
|
try { |
|
return ctx.ok(this.transformer(res.value)); |
|
} catch (e) { |
|
return ctx.err( |
|
`Transmutation failed: ${e instanceof Error ? e.message : e}`, |
|
); |
|
} |
|
} |
|
return res as ValidationResult<U>; |
|
} |
|
} |
|
|
|
export interface CastType<T, U> { |
|
readonly constructor: typeof CastType<T, U>; |
|
} |
|
|
|
export class CastType<T, U> extends Type<U> { |
|
static override readonly name = "Cast<T, U>"; |
|
|
|
constructor( |
|
protected inner: Type<T>, |
|
protected caster: (input: T) => U, |
|
readonly predicate: (it: unknown) => it is U = (_): _ is U => true, |
|
) { |
|
super(`Cast<${inner}, ${caster}>`); |
|
} |
|
|
|
override is(input: unknown): input is U { |
|
return this.inner.is(input) && this.predicate(this.caster(input)); |
|
} |
|
|
|
override as(input: unknown): U { |
|
const t = this.inner.as(input); |
|
return this.caster(t); |
|
} |
|
|
|
override validate(input: unknown, ctx: Context): ValidationResult<U> { |
|
const res = this.inner.validate(input, ctx); |
|
if (res.success) { |
|
try { |
|
return ctx.ok(this.caster(res.value)); |
|
} catch (e) { |
|
return ctx.err( |
|
`Cast failed: ${e instanceof Error ? e.message : e}`, |
|
); |
|
} |
|
} |
|
return res as ValidationResult<U>; |
|
} |
|
} |
|
|
|
export interface RefineType<T, U extends T> { |
|
readonly constructor: typeof RefineType<T, U>; |
|
} |
|
|
|
export class RefineType<T, U extends T> extends Type<U> { |
|
static override readonly name = "Refine<T, U>"; |
|
|
|
static from<T, U extends T, I extends Type<T>>( |
|
inner: I, |
|
refinement: (it: T) => it is U, |
|
info?: Partial<TypeInfoState<T & U, RefineType<T, U>>>, |
|
): RefineType<T, U> & I { |
|
const wrapped = new this(inner, refinement, info) as RefineType<T, U> & I; |
|
ObjectSetPrototypeOf(wrapped, inner); |
|
return wrapped; |
|
} |
|
|
|
constructor( |
|
protected inner: Type<T>, |
|
protected refinement: (it: T) => it is U, |
|
info?: Partial<TypeInfoState<T & U, RefineType<T, U>>>, |
|
) { |
|
super(`Refine<${inner}, ${refinement}>`, info); |
|
} |
|
|
|
override is(input: unknown): input is U { |
|
return this.inner.is(input) && this.refinement(input as T); |
|
} |
|
|
|
override as(input: unknown): U { |
|
const t = this.inner.as(input); |
|
if (this.refinement(t)) return t; |
|
throw new TypeError(`Refinement failed on ${t}`); |
|
} |
|
|
|
override validate(input: unknown, ctx: Context): ValidationResult<U> { |
|
const res = this.inner.validate(input, ctx); |
|
if (res.success) { |
|
if (this.refinement(res.value)) return res as ValidationResult<U>; |
|
return ctx.err(`Refinement failed on ${res.value}`); |
|
} |
|
return res as ValidationResult<U>; |
|
} |
|
} |
|
|
|
export class BrandType<T, K extends PropertyKey = "__brand", U = never> |
|
extends Type<T & { readonly [P in K]: U }> { |
|
static override readonly name = "Brand<T, K, U>"; |
|
constructor( |
|
protected inner: Type<T>, |
|
protected key: K = "__brand" as K, |
|
protected brandValue: U = null! as U, |
|
) { |
|
const keyStr = Type.inspect(key); |
|
const brandStr = Type.inspect(brandValue); |
|
super(`Brand<${inner}, ${keyStr}, ${brandStr}>`); |
|
} |
|
|
|
override is(input: unknown): input is T & { [P in K]: U } { |
|
return this.inner.is(input) && (input as any)[this.key] === this.brandValue; |
|
} |
|
|
|
override as(input: unknown): T & { [P in K]: U } { |
|
const t = this.inner.as(input); |
|
if ((t as any)[this.key] === this.brandValue) { |
|
return t as T & { [P in K]: U }; |
|
} |
|
throw new TypeError( |
|
`Brand check failed: expected property ${ |
|
String(this.key) |
|
} to be ${this.brandValue}`, |
|
); |
|
} |
|
|
|
override validate( |
|
input: unknown, |
|
ctx: Context, |
|
): ValidationResult<T & { [P in K]: U }> { |
|
const res = this.inner.validate(input, ctx); |
|
if (res.success) { |
|
if ((res.value as any)[this.key] === this.brandValue) { |
|
return res as ValidationResult<T & { [P in K]: U }>; |
|
} |
|
return ctx.err( |
|
`Brand check failed: property ${ |
|
String(this.key) |
|
} is not ${this.brandValue}`, |
|
); |
|
} |
|
return res as ValidationResult<T & { [P in K]: U }>; |
|
} |
|
} |
|
|
|
// ---------------------------------------------------------------------------- |
|
// Symbol Types |
|
// ---------------------------------------------------------------------------- |
|
|
|
export interface WellKnownSymbolType { |
|
readonly constructor: typeof WellKnownSymbolType; |
|
} |
|
|
|
export class WellKnownSymbolType extends PrimitiveType<WellKnownSymbol> { |
|
static override readonly name = "WellKnownSymbol"; |
|
|
|
static readonly symbols = [ |
|
SymbolAsyncDispose, |
|
SymbolAsyncIterator, |
|
SymbolDispose, |
|
SymbolHasInstance, |
|
SymbolIsConcatSpreadable, |
|
SymbolIterator, |
|
SymbolMatch, |
|
SymbolMatchAll, |
|
SymbolMetadata, |
|
SymbolReplace, |
|
SymbolSearch, |
|
SymbolSpecies, |
|
SymbolSplit, |
|
SymbolToPrimitive, |
|
SymbolToStringTag, |
|
SymbolUnscopables, |
|
] as const satisfies ReadonlyArray<WellKnownSymbol>; |
|
|
|
static { |
|
ObjectFreeze(this.symbols); |
|
|
|
for (const symbol of this.symbols) { |
|
const name = ( |
|
symbol.description ?? symbol.toString() |
|
).replace(/Symbol\.(\w+)/, "$1"); |
|
|
|
const type = new this({ name, symbol }).refine( |
|
(x) => x === symbol, |
|
`Expected '{input}' to be the well-known symbol '${symbol.toString()}'`, |
|
); |
|
type.info.name(`Symbol.${name}` as const); |
|
(this as typeof WellKnownSymbolType & Record<string, any>)[name] = type; |
|
} |
|
} |
|
|
|
declare static readonly asyncDispose: Type<SymbolAsyncDispose>; |
|
declare static readonly asyncIterator: Type<SymbolAsyncIterator>; |
|
declare static readonly dispose: Type<SymbolDispose>; |
|
declare static readonly hasInstance: Type<SymbolHasInstance>; |
|
declare static readonly isConcatSpreadable: Type<SymbolIsConcatSpreadable>; |
|
declare static readonly iterator: Type<SymbolIterator>; |
|
declare static readonly match: Type<SymbolMatch>; |
|
declare static readonly matchAll: Type<SymbolMatchAll>; |
|
declare static readonly metadata: Type<SymbolMetadata>; |
|
declare static readonly replace: Type<SymbolReplace>; |
|
declare static readonly search: Type<SymbolSearch>; |
|
declare static readonly species: Type<SymbolSpecies>; |
|
declare static readonly split: Type<SymbolSplit>; |
|
declare static readonly toPrimitive: Type<SymbolToPrimitive>; |
|
declare static readonly toStringTag: Type<SymbolToStringTag>; |
|
declare static readonly unscopables: Type<SymbolUnscopables>; |
|
|
|
constructor( |
|
info?: Partial<TypeInfoState<WellKnownSymbol, WellKnownSymbolType>>, |
|
) { |
|
super("symbol", { ...info, validator: isWellKnownSymbol }); |
|
this.info.name(info?.name ?? "WellKnownSymbol"); |
|
} |
|
|
|
override toString(): string { |
|
return this.info.getName() || "WellKnownSymbol"; |
|
} |
|
} |
|
|
|
export const wellKnownSymbol: WellKnownSymbolType = new WellKnownSymbolType(); |
|
|
|
export interface RegisteredSymbolType< |
|
T extends RegisteredSymbol = RegisteredSymbol, |
|
> { |
|
readonly constructor: typeof RegisteredSymbolType<T>; |
|
} |
|
|
|
export class RegisteredSymbolType< |
|
T extends RegisteredSymbol = RegisteredSymbol, |
|
> extends Type<T> { |
|
static override readonly name = "RegisteredSymbol"; |
|
|
|
constructor(info?: Partial<TypeInfoState<T, RegisteredSymbolType<T>>>) { |
|
super("RegisteredSymbol", info); |
|
this.info.validator( |
|
isRegisteredSymbol as unknown as (it: unknown) => it is T, |
|
); |
|
} |
|
} |
|
|
|
export const registeredSymbol: RegisteredSymbolType = |
|
new RegisteredSymbolType(); |
|
|
|
export interface UniqueSymbolType< |
|
T extends UniqueSymbol = UniqueSymbol, |
|
> { |
|
readonly constructor: typeof UniqueSymbolType<T>; |
|
} |
|
|
|
export class UniqueSymbolType< |
|
T extends UniqueSymbol = UniqueSymbol, |
|
> extends Type<T> { |
|
static override readonly name = "UniqueSymbol"; |
|
|
|
constructor(info?: Partial<TypeInfoState<T, UniqueSymbolType<T>>>) { |
|
super("UniqueSymbol", info); |
|
this.info.validator(isUniqueSymbol as unknown as (it: unknown) => it is T); |
|
} |
|
} |
|
|
|
export const uniqueSymbol: UniqueSymbolType = new UniqueSymbolType(); |
|
|
|
// ---------------------------------------------------------------------------- |
|
// Optional / Nullable Types |
|
// ---------------------------------------------------------------------------- |
|
|
|
export interface OptionalType<T> { |
|
readonly constructor: typeof OptionalType<T>; |
|
} |
|
|
|
export class OptionalType<T> extends Type<T | undefined> { |
|
constructor( |
|
protected inner: Type<T>, |
|
info?: Partial<TypeInfoState<T | undefined>>, |
|
) { |
|
const validator = (x: unknown): x is T | undefined => |
|
x === void 0 || (this.inner.is(x) && info?.validator?.(x) !== false); |
|
|
|
super(`${inner} | undefined`, { ...info, optional: true, validator }); |
|
} |
|
|
|
override as(input: unknown): T | undefined { |
|
return input === void 0 ? void 0 : this.inner.as(input); |
|
} |
|
|
|
override validate( |
|
input: unknown, |
|
ctx: Context, |
|
): ValidationResult<T | undefined> { |
|
if (input === void 0) return ctx.ok(void 0); |
|
return this.inner.validate(input, ctx); |
|
} |
|
} |
|
|
|
export function optional<T>(inner: Type<T>): OptionalType<T> { |
|
return new OptionalType(inner); |
|
} |
|
|
|
export interface NullableType<T> { |
|
readonly constructor: typeof NullableType<T>; |
|
} |
|
|
|
export class NullableType<T> extends Type<T | null> { |
|
constructor(protected inner: Type<T>) { |
|
super(`${inner} | null`, { nullable: true }); |
|
} |
|
|
|
override is(input: unknown): input is T | null { |
|
return input === null || this.inner.is(input); |
|
} |
|
|
|
override as(input: unknown): T | null { |
|
return input === null ? null : this.inner.as(input); |
|
} |
|
|
|
override validate(input: unknown, ctx: Context): ValidationResult<T | null> { |
|
if (input === null) return ctx.ok(null); |
|
return this.inner.validate(input, ctx); |
|
} |
|
} |
|
|
|
export function nullable<T>(inner: Type<T>): NullableType<T> { |
|
return new NullableType(inner); |
|
} |
|
|
|
export interface NonNullableType<T> { |
|
readonly constructor: typeof NonNullableType<T>; |
|
} |
|
|
|
export class NonNullableType<T> extends Type<NonNullable<T>> { |
|
constructor(protected inner: Type<T>) { |
|
super(`NonNullable<${inner}>`, { nullable: false }); |
|
} |
|
|
|
override is(input: unknown): input is NonNullable<T> { |
|
return input != null && this.inner.is(input); |
|
} |
|
|
|
override as(input: unknown): NonNullable<T> { |
|
if (input === null || input === undefined) { |
|
throw new Error(`Value is null or undefined for nonNullable type`); |
|
} |
|
return this.inner.as(input) as NonNullable<T>; |
|
} |
|
|
|
override validate( |
|
input: unknown, |
|
ctx: Context, |
|
): ValidationResult<NonNullable<T>> { |
|
if (!this.is(input)) { |
|
return ctx.err`Cannot assign a value of ${input} to ${this}`; |
|
} |
|
return this.inner.validate(input, ctx) as ValidationResult< |
|
NonNullable<T> |
|
>; |
|
} |
|
} |
|
|
|
export function notnull<T>(inner: Type<T>): NonNullableType<T> { |
|
return new NonNullableType(inner); |
|
} |
|
|
|
// ---------------------------------------------------------------------------- |
|
// Native Classes |
|
// ---------------------------------------------------------------------------- |
|
|
|
export type DateLike = string | number | Date; |
|
|
|
export interface DateType { |
|
readonly constructor: typeof DateType; |
|
} |
|
|
|
export class DateType extends Type<Date> { |
|
static override readonly name = "Date"; |
|
|
|
protected value: Date | null = null; |
|
|
|
constructor(value: DateLike | null = null) { |
|
if (value != null) { |
|
value = new Date(value); |
|
if (isNaN(value.getTime())) throw new TypeError("Invalid Date"); |
|
} |
|
super(value === null ? "Date" : `Date<${value}>`); |
|
this.value = value ?? null; |
|
} |
|
|
|
override is(input: unknown): input is Date { |
|
return isDate(input) && ( |
|
this.value === null || this.value.getTime() === input.getTime() |
|
); |
|
} |
|
} |
|
|
|
export const date: DateType = new DateType(); |
|
|
|
export interface RegExpType { |
|
readonly constructor: typeof RegExpType; |
|
} |
|
|
|
export class RegExpType extends Type<RegExp> { |
|
static override readonly name = "RegExp"; |
|
|
|
constructor(info?: Partial<TypeInfoState<RegExp>>) { |
|
super("RegExp", { ...info, validator: isRegExp }); |
|
} |
|
} |
|
|
|
export const regexp: RegExpType = new RegExpType(); |
|
|
|
export const regExp: RegExpType = regexp; |
|
|
|
export interface ErrorType { |
|
readonly constructor: typeof ErrorType; |
|
} |
|
|
|
export class ErrorType extends Type<Error> { |
|
static override readonly name = "Error"; |
|
|
|
constructor(info?: Partial<TypeInfoState<Error>>) { |
|
super("Error", { ...info, validator: isError }); |
|
} |
|
} |
|
|
|
export const error: ErrorType = new ErrorType(); |
|
|
|
export interface PromiseType<T = any> { |
|
readonly constructor: typeof PromiseType<T>; |
|
} |
|
|
|
export class PromiseType<T = any> extends Type<Promise<T>> { |
|
static override readonly name = "Promise"; |
|
|
|
constructor(info?: Partial<TypeInfoState<Promise<T>>>) { |
|
super("Promise", { ...info, validator: isPromise }); |
|
} |
|
} |
|
|
|
export const promise: PromiseType = new PromiseType(); |
|
|
|
// ---------------------------------------------------------------------------- |
|
// Structured Data (Binary) |
|
// ---------------------------------------------------------------------------- |
|
|
|
export interface ArrayBufferType { |
|
readonly constructor: typeof ArrayBufferType; |
|
} |
|
|
|
export class ArrayBufferType extends Type<ArrayBuffer> { |
|
static override readonly name = "ArrayBuffer"; |
|
|
|
constructor(info?: Partial<TypeInfoState<ArrayBuffer>>) { |
|
super("ArrayBuffer", { ...info, validator: isArrayBuffer }); |
|
} |
|
} |
|
|
|
export const arrayBuffer: Type<ArrayBuffer> = new ArrayBufferType(); |
|
|
|
export interface DataViewType { |
|
readonly constructor: typeof DataViewType; |
|
} |
|
|
|
export class DataViewType extends Type<DataView> { |
|
static override readonly name = "DataView"; |
|
|
|
constructor(info?: Partial<TypeInfoState<DataView>>) { |
|
super("DataView", { ...info, validator: isDataView }); |
|
} |
|
} |
|
|
|
export const dataView: Type<DataView> = new DataViewType(); |
|
|
|
export class TypedArrayType< |
|
K extends TypedArrayTypeName = TypedArrayTypeName, |
|
> extends Type<TypedArrayTypeMap[K]> { |
|
constructor( |
|
override readonly name: K | "TypedArray" = "TypedArray", |
|
info?: Partial<TypeInfoState<TypedArrayTypeMap[K]>>, |
|
) { |
|
super(name ?? "", { |
|
...info, |
|
validator: (x: unknown): x is TypedArrayTypeMap[K] => |
|
!name || name === "TypedArray" |
|
? isTypedArray(x) |
|
: isTypedArray(x, name), |
|
}); |
|
} |
|
|
|
override is(input: unknown): input is TypedArrayTypeMap[K] { |
|
if (this.name === "TypedArray") return isTypedArray(input); |
|
return isTypedArray(input, this.name); |
|
} |
|
} |
|
|
|
export interface Uint8ArrayType { |
|
readonly constructor: typeof Uint8ArrayType; |
|
} |
|
|
|
export class Uint8ArrayType extends TypedArrayType<"Uint8Array"> { |
|
static override readonly name = "Uint8Array"; |
|
|
|
constructor(info?: Partial<TypeInfoState<Uint8Array>>) { |
|
super("Uint8Array", { ...info, validator: isUint8Array }); |
|
} |
|
} |
|
|
|
export const uint8Array: Uint8ArrayType = new Uint8ArrayType(); |
|
|
|
export interface Uint8ClampedArrayType { |
|
readonly constructor: typeof Uint8ClampedArrayType; |
|
} |
|
|
|
export class Uint8ClampedArrayType extends TypedArrayType<"Uint8ClampedArray"> { |
|
static override readonly name = "Uint8ClampedArray"; |
|
|
|
constructor(info?: Partial<TypeInfoState<Uint8ClampedArray>>) { |
|
super("Uint8ClampedArray", { ...info, validator: isUint8ClampedArray }); |
|
} |
|
} |
|
|
|
export const uint8ClampedArray: Uint8ClampedArrayType = |
|
new Uint8ClampedArrayType(); |
|
|
|
export interface Uint16ArrayType { |
|
readonly constructor: typeof Uint16ArrayType; |
|
} |
|
export class Uint16ArrayType extends TypedArrayType<"Uint16Array"> { |
|
static override readonly name = "Uint16Array"; |
|
constructor(info?: Partial<TypeInfoState<Uint16Array>>) { |
|
super("Uint16Array", { ...info, validator: isUint16Array }); |
|
} |
|
} |
|
export const uint16Array: Uint16ArrayType = new Uint16ArrayType(); |
|
|
|
export interface Uint32ArrayType { |
|
readonly constructor: typeof Uint32ArrayType; |
|
} |
|
export class Uint32ArrayType extends TypedArrayType<"Uint32Array"> { |
|
static override readonly name = "Uint32Array"; |
|
constructor(info?: Partial<TypeInfoState<Uint32Array>>) { |
|
super("Uint32Array", { ...info, validator: isUint32Array }); |
|
} |
|
} |
|
export const uint32Array: Uint32ArrayType = new Uint32ArrayType(); |
|
|
|
export interface Int8ArrayType { |
|
readonly constructor: typeof Int8ArrayType; |
|
} |
|
export class Int8ArrayType extends TypedArrayType<"Int8Array"> { |
|
static override readonly name = "Int8Array"; |
|
constructor(info?: Partial<TypeInfoState<Int8Array>>) { |
|
super("Int8Array", { ...info, validator: isInt8Array }); |
|
} |
|
} |
|
export const int8Array: Int8ArrayType = new Int8ArrayType(); |
|
|
|
export interface Int16ArrayType { |
|
readonly constructor: typeof Int16ArrayType; |
|
} |
|
export class Int16ArrayType extends TypedArrayType<"Int16Array"> { |
|
static override readonly name = "Int16Array"; |
|
constructor(info?: Partial<TypeInfoState<Int16Array>>) { |
|
super("Int16Array", { ...info, validator: isInt16Array }); |
|
} |
|
} |
|
export const int16Array: Int16ArrayType = new Int16ArrayType(); |
|
|
|
export interface Int32ArrayType { |
|
readonly constructor: typeof Int32ArrayType; |
|
} |
|
export class Int32ArrayType extends TypedArrayType<"Int32Array"> { |
|
static override readonly name = "Int32Array"; |
|
constructor(info?: Partial<TypeInfoState<Int32Array>>) { |
|
super("Int32Array", { ...info, validator: isInt32Array }); |
|
} |
|
} |
|
export const int32Array: Int32ArrayType = new Int32ArrayType(); |
|
|
|
export interface Float16ArrayType { |
|
readonly constructor: typeof Float16ArrayType; |
|
} |
|
export class Float16ArrayType extends TypedArrayType<"Float16Array"> { |
|
static override readonly name = "Float16Array"; |
|
constructor(info?: Partial<TypeInfoState<Float16Array>>) { |
|
super("Float16Array", { ...info, validator: isFloat16Array }); |
|
} |
|
} |
|
export const float16Array: Float16ArrayType = new Float16ArrayType(); |
|
|
|
export interface Float32ArrayType { |
|
readonly constructor: typeof Float32ArrayType; |
|
} |
|
export class Float32ArrayType extends TypedArrayType<"Float32Array"> { |
|
static override readonly name = "Float32Array"; |
|
constructor(info?: Partial<TypeInfoState<Float32Array>>) { |
|
super("Float32Array", { ...info, validator: isFloat32Array }); |
|
} |
|
} |
|
export const float32Array: Float32ArrayType = new Float32ArrayType(); |
|
|
|
export interface Float64ArrayType { |
|
readonly constructor: typeof Float64ArrayType; |
|
} |
|
export class Float64ArrayType extends TypedArrayType<"Float64Array"> { |
|
static override readonly name = "Float64Array"; |
|
constructor(info?: Partial<TypeInfoState<Float64Array>>) { |
|
super("Float64Array", { ...info, validator: isFloat64Array }); |
|
} |
|
} |
|
export const float64Array: Float64ArrayType = new Float64ArrayType(); |
|
|
|
export interface BigInt64ArrayType { |
|
readonly constructor: typeof BigInt64ArrayType; |
|
} |
|
export class BigInt64ArrayType extends TypedArrayType<"BigInt64Array"> { |
|
static override readonly name = "BigInt64Array"; |
|
constructor(info?: Partial<TypeInfoState<BigInt64Array>>) { |
|
super("BigInt64Array", { ...info, validator: isBigInt64Array }); |
|
} |
|
} |
|
export const bigInt64Array: BigInt64ArrayType = new BigInt64ArrayType(); |
|
|
|
export interface BigUint64ArrayType { |
|
readonly constructor: typeof BigUint64ArrayType; |
|
} |
|
|
|
export class BigUint64ArrayType extends TypedArrayType<"BigUint64Array"> { |
|
static override readonly name = "BigUint64Array"; |
|
constructor(info?: Partial<TypeInfoState<BigUint64Array>>) { |
|
super("BigUint64Array", { ...info, validator: isBigUint64Array }); |
|
} |
|
} |
|
export const bigUint64Array: BigUint64ArrayType = new BigUint64ArrayType(); |
|
|
|
// ---------------------------------------------------------------------------- |
|
// Control-flow and Iteration |
|
// ---------------------------------------------------------------------------- |
|
|
|
export interface IteratorType<T = any> { |
|
readonly constructor: typeof IteratorType<T>; |
|
} |
|
|
|
export class IteratorType<T = any> extends Type<Iterator<T>> { |
|
static override readonly name = "Iterator"; |
|
|
|
constructor(info?: Partial<TypeInfoState<Iterator<T>>>) { |
|
super("Iterator", info); |
|
} |
|
|
|
override is(input: unknown): input is Iterator<T> { |
|
return typeof input === "object" && input != null && |
|
typeof (input as Iterator<T>).next === "function"; |
|
} |
|
} |
|
export const iterator: IteratorType = new IteratorType(); |
|
|
|
export interface IterableType<T = any> { |
|
readonly constructor: typeof IterableType<T>; |
|
} |
|
|
|
export class IterableType<T = any> extends Type<Iterable<T>> { |
|
static override readonly name = "Iterable<T>"; |
|
|
|
constructor( |
|
protected valueType: Type<T> = any, |
|
info?: Partial<TypeInfoState<Iterable<T>>>, |
|
) { |
|
super(`Iterable<${valueType}>`, info); |
|
this.info.validator( |
|
(input): input is Iterable<T> => { |
|
if (!isIterable(input)) return false; |
|
let i = 0; |
|
for (const value of input) { |
|
if (!valueType.is(value)) return false; |
|
if (i++ > 1000) break; // Prevent infinite loops |
|
} |
|
return true; |
|
}, |
|
); |
|
} |
|
} |
|
export const iterable: IterableType = new IterableType(); |
|
|
|
export interface IterableIteratorType< |
|
T = any, |
|
TReturn = undefined, |
|
TNext = undefined, |
|
> { |
|
readonly constructor: typeof IterableIteratorType<T, TReturn, TNext>; |
|
} |
|
|
|
export class IterableIteratorType< |
|
T = any, |
|
TReturn = undefined, |
|
TNext = undefined, |
|
> extends Type<IterableIterator<T, TReturn, TNext>> { |
|
static override readonly name = "IterableIterator<T, TReturn, TNext>"; |
|
|
|
constructor( |
|
readonly valueType: Type<T> = any, |
|
info?: Partial<TypeInfoState<IterableIterator<T, TReturn, TNext>>>, |
|
) { |
|
super(`IterableIterator<${valueType}>`, info); |
|
this.info.validator( |
|
(input): input is IterableIterator<T, TReturn, TNext> => { |
|
if (!isIterableIterator(input)) return false; |
|
let i = 0; |
|
for (const value of input) { |
|
if (!valueType.is(value)) return false; |
|
if (i++ > 1000) break; // Prevent infinite loops |
|
} |
|
return true; |
|
}, |
|
); |
|
} |
|
} |
|
|
|
export const iterableIterator: IterableIteratorType = |
|
new IterableIteratorType(); |
|
|
|
export interface GeneratorType<T = unknown, TReturn = any, TNext = any> { |
|
readonly constructor: typeof GeneratorType<T, TReturn, TNext>; |
|
} |
|
|
|
export class GeneratorType<T = unknown, TReturn = any, TNext = any> |
|
extends Type<Generator<T, TReturn, TNext>> { |
|
static override readonly name = "Generator<T, TReturn, TNext>"; |
|
|
|
constructor( |
|
readonly valueType: Type<T> = any, |
|
info?: Partial<TypeInfoState<Generator<T, TReturn, TNext>>>, |
|
) { |
|
super(`Generator<${valueType}>`, info); |
|
this.info.validator( |
|
(input): input is Generator<T, TReturn, TNext> => { |
|
if (!isGenerator(input)) return false; |
|
let i = 0; |
|
for (const value of input) { |
|
if (!valueType.is(value)) return false; |
|
if (i++ > 1000) break; // Prevent infinite loops |
|
} |
|
return true; |
|
}, |
|
); |
|
} |
|
} |
|
export const generator: GeneratorType = new GeneratorType(); |
|
|
|
export interface GeneratorFunctionType { |
|
readonly constructor: typeof GeneratorFunctionType; |
|
} |
|
|
|
export class GeneratorFunctionType extends Type<GeneratorFunction> { |
|
static override readonly name = "GeneratorFunction"; |
|
|
|
constructor(info?: Partial<TypeInfoState<GeneratorFunction>>) { |
|
super("GeneratorFunction", { ...info, validator: isGeneratorFunction }); |
|
} |
|
} |
|
|
|
export const generatorFunction: GeneratorFunctionType = |
|
new GeneratorFunctionType(); |
|
|
|
export class AsyncGeneratorType<T = unknown, TReturn = any, TNext = any> |
|
extends Type<AsyncGenerator<T, TReturn, TNext>> { |
|
static override readonly name = "AsyncGenerator<T, TReturn, TNext>"; |
|
|
|
constructor( |
|
info?: Partial<TypeInfoState<AsyncGenerator<T, TReturn, TNext>>>, |
|
) { |
|
super("AsyncGenerator<T, TReturn, TNext>", { |
|
...info, |
|
validator: isAsyncGenerator as any, |
|
}); |
|
} |
|
} |
|
export const asyncGenerator: AsyncGeneratorType = new AsyncGeneratorType(); |
|
|
|
export interface AsyncGeneratorFunctionType { |
|
readonly constructor: typeof AsyncGeneratorFunctionType; |
|
} |
|
|
|
export class AsyncGeneratorFunctionType extends Type<AsyncGeneratorFunction> { |
|
static override readonly name = "AsyncGeneratorFunction"; |
|
|
|
constructor(info?: Partial<TypeInfoState<AsyncGeneratorFunction>>) { |
|
super("AsyncGeneratorFunction", { |
|
...info, |
|
validator: isAsyncGeneratorFunction, |
|
}); |
|
} |
|
} |
|
|
|
export const asyncGeneratorFunction: Type<AsyncGeneratorFunction> = |
|
new AsyncGeneratorFunctionType(); |
|
|
|
export interface AsyncIterableType< |
|
T = any, |
|
TReturn = undefined, |
|
TNext = undefined, |
|
> { |
|
readonly constructor: typeof AsyncIterableType<T, TReturn, TNext>; |
|
} |
|
|
|
export class AsyncIterableType<T = any, TReturn = undefined, TNext = undefined> |
|
extends Type<AsyncIterable<T, TReturn, TNext>> { |
|
static override readonly name = "AsyncIterable<T, TReturn, TNext>"; |
|
|
|
constructor( |
|
info?: Partial<TypeInfoState<AsyncIterable<T, TReturn, TNext>>>, |
|
) { |
|
// TODO: add support for valueType (and asynchronous validation ... ?) |
|
super(`AsyncIterable<T, TReturn, TNext>`, { |
|
...info, |
|
validator: isAsyncIterable, |
|
}); |
|
} |
|
} |
|
|
|
export const asyncIterable: AsyncIterableType = new AsyncIterableType(); |
|
|
|
export interface AsyncIterableIteratorType< |
|
T = any, |
|
TReturn = undefined, |
|
TNext = undefined, |
|
> { |
|
readonly constructor: typeof AsyncIterableIteratorType<T, TReturn, TNext>; |
|
} |
|
|
|
export class AsyncIterableIteratorType< |
|
T = any, |
|
TReturn = undefined, |
|
TNext = undefined, |
|
> extends Type<AsyncIterableIterator<T, TReturn, TNext>> { |
|
static override readonly name = "AsyncIterableIterator<T, TReturn, TNext>"; |
|
|
|
constructor( |
|
info?: Partial<TypeInfoState<AsyncIterableIterator<T, TReturn, TNext>>>, |
|
) { |
|
super("AsyncIterableIterator<T, TReturn, TNext>", { |
|
...info, |
|
validator: isAsyncIterableIterator, |
|
}); |
|
} |
|
} |
|
export const asyncIterableIterator: AsyncIterableIteratorType = |
|
new AsyncIterableIteratorType(); |
|
|
|
export interface AsyncIteratorType< |
|
T = any, |
|
TReturn = undefined, |
|
TNext = undefined, |
|
> { |
|
readonly constructor: typeof AsyncIteratorType<T, TReturn, TNext>; |
|
} |
|
|
|
export class AsyncIteratorType<T = any, TReturn = undefined, TNext = undefined> |
|
extends Type<AsyncIterator<T>> { |
|
static override readonly name = "AsyncIterator<T, TReturn, TNext>"; |
|
|
|
constructor(info?: Partial<TypeInfoState<AsyncIterator<T, TReturn, TNext>>>) { |
|
super("AsyncIterator<T, TReturn, TNext>", { |
|
...info, |
|
validator: isAsyncIterator, |
|
}); |
|
} |
|
} |
|
export const asyncIterator: AsyncIteratorType = new AsyncIteratorType(); |
|
|
|
export interface AsyncFunction<T = any> extends Function { |
|
(...args: any[]): Promise<T>; |
|
} |
|
|
|
export interface AsyncFunctionType<T = any> { |
|
readonly constructor: typeof AsyncFunctionType<T>; |
|
} |
|
export class AsyncFunctionType<T = any> extends Type<AsyncFunction<T>> { |
|
static override readonly name = "AsyncFunction"; |
|
|
|
constructor(info?: Partial<TypeInfoState<AsyncFunction<T>>>) { |
|
super("AsyncFunction", { ...info, validator: isAsyncFunction }); |
|
} |
|
|
|
override as(input: unknown): AsyncFunction<T> { |
|
if (this.is(input)) return input; |
|
if (isFunction(input)) { |
|
return async function (this: unknown, ...args) { |
|
const result = FunctionPrototypeApply( |
|
input as AsyncFunction, |
|
this, |
|
args, |
|
); |
|
return await Promise.resolve(result); |
|
}; |
|
} |
|
throw new TypeError("Expected an AsyncFunction or a Function"); |
|
} |
|
} |
|
|
|
export const asyncFunction: AsyncFunctionType = new AsyncFunctionType(); |
|
|
|
export interface FunctionType { |
|
readonly constructor: typeof FunctionType; |
|
} |
|
|
|
export class FunctionType extends Type<Function> { |
|
static override readonly name = "Function"; |
|
|
|
constructor(info?: Partial<TypeInfoState<Function>>) { |
|
super("Function", { ...info, validator: isFunction }); |
|
} |
|
} |
|
|
|
export const fn: FunctionType = new FunctionType(); |
|
|
|
export { fn as function }; |
|
|
|
// ---------------------------------------------------------------------------- |
|
// Keyed Collections |
|
// ---------------------------------------------------------------------------- |
|
|
|
export interface MapType<K, V> { |
|
<K2 extends K, V2 extends V>( |
|
keyType?: Type<K2>, |
|
valueType?: Type<V2>, |
|
info?: Partial<TypeInfoState<Map<K2, V2>>>, |
|
): MapType<K2, V2>; |
|
|
|
readonly constructor: typeof MapType<K, V>; |
|
} |
|
|
|
export class MapType<K, V> extends Type<Map<K, V>> { |
|
constructor( |
|
protected keyType: Type<K> = any, |
|
protected valueType: Type<V> = any, |
|
info?: Partial<TypeInfoState<Map<K, V>>>, |
|
) { |
|
super(`Map<${keyType}, ${valueType}>`, { ...info, validator: isMap }); |
|
} |
|
|
|
override is(input: unknown): input is Map<K, V> { |
|
if (isMap(input)) { |
|
for (const [key, value] of input) { |
|
if (!this.keyType.is(key) || !this.valueType.is(value)) return false; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
override validate( |
|
input: unknown, |
|
ctx: Context, |
|
): ValidationResult<Map<K, V>> { |
|
const { keyType, valueType } = this; |
|
|
|
if (isMap<K, V>(input)) { |
|
let i = 0; |
|
for (const [k, v] of input) { |
|
if (!keyType.is(k)) { |
|
return ctx |
|
.err`Key type '${k}' is not assignable to map key type '${keyType}'.`; |
|
} |
|
if (!valueType.is(v)) { |
|
const vc = ctx.with(Type.inspect(k)); |
|
return vc.err`Type '${v}' is not assignable to type '${valueType}'.`; |
|
} |
|
i++; |
|
} |
|
|
|
return ctx.ok(input); |
|
} |
|
|
|
return ctx.err`Expected Map<${keyType}, ${valueType}>`; |
|
} |
|
} |
|
|
|
export const map: MapType<any, any> = new MapType(); |
|
|
|
export interface SetType<T = any> { |
|
readonly constructor: typeof SetType<T>; |
|
} |
|
|
|
export class SetType<T = any> extends Type<Set<T>> { |
|
constructor( |
|
protected valueType: Type<T> = any, |
|
info?: Partial<TypeInfoState<Set<T>>>, |
|
) { |
|
super(`Set<${valueType}>`, { ...info, validator: isSet }); |
|
} |
|
|
|
override is(input: unknown): input is Set<T> { |
|
if (isSet(input)) { |
|
for (const value of input) { |
|
if (!this.valueType.is(value)) return false; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
override validate( |
|
input: unknown, |
|
ctx: Context<Set<T>>, |
|
): ValidationResult<Set<T>> { |
|
const { valueType } = this; |
|
|
|
if (isSet<T>(input)) { |
|
let i = 0; |
|
for (const v of input) { |
|
if (!valueType.is(v)) { |
|
const valueCtx = ctx.with(`[${i}]`); |
|
return valueCtx |
|
.err`Type '${v}' is not assignable to type '${valueType}' (index ${i})`; |
|
} |
|
i++; |
|
} |
|
|
|
return ctx.ok(input); |
|
} |
|
|
|
return ctx.err`Type '${input}' is not assignable to type '${this}'`; |
|
} |
|
} |
|
export const set: SetType<any> = new SetType(); |
|
|
|
export interface WeakKeyType { |
|
readonly constructor: typeof WeakKeyType; |
|
} |
|
|
|
export class WeakKeyType extends Type<WeakKey> { |
|
static override readonly name = "WeakKey"; |
|
|
|
constructor(info?: Partial<TypeInfoState<WeakKey>>) { |
|
super("WeakKey", { ...info, validator: isWeakKey }); |
|
} |
|
|
|
override validate( |
|
input: unknown, |
|
ctx: Context<WeakKey>, |
|
): ValidationResult<WeakKey>; |
|
override validate(input: unknown, ctx: Context): ValidationResult { |
|
if (this.is(input)) { |
|
return ctx.ok(input); |
|
} else { |
|
return ctx |
|
.err`Expected a WeakKey value, which includes all objects/arrays/functions. ES2023+ runtimes also support non-registered symbols (not created with Symbol.for) as weak keys.`; |
|
} |
|
} |
|
} |
|
|
|
export const weakKey: WeakKeyType = new WeakKeyType(); |
|
|
|
export class WeakMapType<K extends WeakKey = WeakKey, V = any> |
|
extends Type<WeakMap<K, V>> { |
|
static override readonly name = "WeakMap"; |
|
|
|
constructor(info?: Partial<TypeInfoState<WeakMap<K, V>>>) { |
|
super("WeakMap", { ...info, validator: isWeakMap }); |
|
} |
|
} |
|
|
|
export const weakMap: WeakMapType = new WeakMapType(); |
|
|
|
export interface WeakSetType<T extends WeakKey = WeakKey> { |
|
readonly constructor: typeof WeakSetType<T>; |
|
} |
|
|
|
export class WeakSetType<T extends WeakKey = WeakKey> extends Type<WeakSet<T>> { |
|
static override readonly name = "WeakSet"; |
|
|
|
constructor(info?: Partial<TypeInfoState<WeakSet<T>>>) { |
|
super("WeakSet", { ...info, validator: isWeakSet }); |
|
} |
|
} |
|
|
|
export const weakSet: WeakSetType = new WeakSetType(); |
|
|
|
export interface WeakRefType<T extends WeakKey = WeakKey> { |
|
readonly constructor: typeof WeakRefType<T>; |
|
} |
|
|
|
export class WeakRefType<T extends WeakKey = WeakKey> extends Type<WeakRef<T>> { |
|
static override readonly name = "WeakRef"; |
|
|
|
constructor( |
|
protected valueType: Type<T> = weakKey as unknown as Type<T>, |
|
info?: Partial<TypeInfoState<WeakRef<T>>>, |
|
) { |
|
super("WeakRef", { ...info, validator: isWeakRef }); |
|
|
|
if (valueType !== (weakKey as unknown as Type<T>)) { |
|
this.info.constraint( |
|
"valueType", |
|
valueType.is.bind(valueType), |
|
`Type '{input}' is not assignable to type '${valueType}'`, |
|
); |
|
} |
|
} |
|
} |
|
export const weakRef: WeakRefType = new WeakRefType(); |
|
|
|
// ---------------------------------------------------------------------------- |
|
// Composite and Higher-Order Types |
|
// ---------------------------------------------------------------------------- |
|
|
|
export interface UnionType<A, B> { |
|
readonly constructor: typeof UnionType<A, B>; |
|
} |
|
|
|
export class UnionType<A, B> extends Type<A | B> { |
|
static override readonly name = "Union<A, B>"; |
|
|
|
constructor(protected left: Type<A>, protected right: Type<B>) { |
|
super(`${left} | ${right}`); |
|
} |
|
|
|
override is(input: unknown): input is A | B { |
|
return this.left.is(input) || this.right.is(input); |
|
} |
|
|
|
override as(input: unknown): A | B { |
|
if (this.left.is(input)) return this.left.as(input); |
|
if (this.right.is(input)) return this.right.as(input); |
|
throw new ValidationError(`Invalid value for ${this}`); |
|
} |
|
|
|
override validate(v: unknown, ctx: Context<A | B>): ValidationResult<A | B>; |
|
override validate(input: unknown, ctx: Context): ValidationResult { |
|
const a = this.left.validate(input, ctx); |
|
if (a.success) return a; |
|
const b = this.right.validate(input, ctx); |
|
if (b.success) return b; |
|
return ctx.err`Type '${input}' is not assignable to union type '${this}'`; |
|
} |
|
} |
|
|
|
export interface IntersectionType<A, B> { |
|
readonly constructor: typeof IntersectionType<A, B>; |
|
} |
|
|
|
export class IntersectionType<A, B> extends Type<A & B> { |
|
constructor( |
|
protected left: Type<A>, |
|
protected right: Type<B>, |
|
info?: Partial<TypeInfoState<A & B>>, |
|
) { |
|
super(`${left} & ${right}`, info); |
|
} |
|
|
|
override is(input: unknown): input is A & B { |
|
return this.left.is(input) && this.right.is(input); |
|
} |
|
|
|
override as(input: unknown): A & B { |
|
const a = this.left.as(input), b = this.right.as(input); |
|
if ((isFunction(a) || isObject(a)) && (isFunction(b) || isObject(b))) { |
|
return Object.assign({}, a, b); |
|
} else { |
|
return b as A & B; |
|
} |
|
} |
|
|
|
override validate( |
|
input: unknown, |
|
ctx: Context<A & B>, |
|
): ValidationResult<A & B>; |
|
override validate(input: unknown, ctx: Context): ValidationResult { |
|
const a = this.left.validate(input, ctx); |
|
if (!a.success) return a; |
|
const b = this.right.validate(input, ctx); |
|
if (!b.success) return b; |
|
if ( |
|
typeof a.value === "object" && a.value !== null && |
|
typeof b.value === "object" && b.value !== null |
|
) { |
|
return ctx.ok(Object.assign({}, a.value, b.value)); |
|
} |
|
return ctx.ok(b.value as A & B); |
|
} |
|
} |
|
|
|
|
|
// ---------------------------------------------------------------------------- |
|
// Object and Array Types |
|
// ---------------------------------------------------------------------------- |
|
|
|
export interface ObjectType<T extends Record<string, Type<any>>> { |
|
readonly constructor: typeof ObjectType<T>; |
|
} |
|
|
|
export class ObjectType<const T extends Record<string, Type<any>>> |
|
extends Type<Type.infer<T>> { |
|
static override readonly name = "Object<T>"; |
|
|
|
@lru({ maxSize: 1024 }) |
|
static render<T extends Record<string, unknown>>( |
|
schema: T, |
|
options: RenderOptions = { ...defaultRenderOptions }, |
|
): string { |
|
const opt = { ...defaultRenderOptions, ...options }; |
|
const keys = Object.keys(schema) as (keyof T)[]; |
|
let multiline = false, sp = " ", s = ""; |
|
s += "{"; |
|
for (let i = 0; i < keys.length; i++) { |
|
const key = keys[i], type = schema[key]; |
|
const def = type instanceof Type ? type.toString() : Type.inspect(type); |
|
if (multiline || s.length > opt.lineWidth - 2) { |
|
multiline = true; |
|
sp = opt.useTabs ? "\t" : " ".repeat(opt.indentWidth); |
|
sp = sp.repeat(++opt.indent); |
|
sp = "\n" + sp; |
|
} |
|
s += sp; |
|
let k = key.toString(); |
|
if (!/^[$_\p{ID_Start}][$_\p{ID_Continue}\u{200C}\u{200D}]*$/u.test(k)) { |
|
k = JSON.stringify(k); |
|
} |
|
if (opt.singleQuote) k = k.replace(/"/g, "'"); |
|
s += `${k}: ${def}`; |
|
if (i === keys.length - 1) { |
|
if (multiline) { |
|
s += `${opt.trailingComma ? "," : ""}\n`; |
|
} else { |
|
s += " "; |
|
} |
|
} else { |
|
s += ","; |
|
} |
|
} |
|
s += "}"; |
|
return s; |
|
} |
|
|
|
constructor( |
|
readonly schema: T, |
|
info?: Partial<TypeInfoState<Type.infer<T>>>, |
|
) { |
|
super("object", info); |
|
} |
|
|
|
override is(input: unknown): input is Type.infer<T> { |
|
if (typeof input !== "object" || input === null) return false; |
|
for (const key in this.schema) { |
|
const checker = this.schema[key]; |
|
if (!(checker && checker instanceof Type)) continue; |
|
if (!checker.is((input as any)[key])) return false; |
|
} |
|
return true; |
|
} |
|
|
|
override as(input: unknown): Type.infer<T> { |
|
if (typeof input !== "object" || input === null) { |
|
throw new TypeError(`Expected object, got ${typeof input}`); |
|
} |
|
const result = { __proto__: null } as unknown as Type.infer<T>; |
|
for (const key in this.schema) { |
|
const checker = this.schema[key]; |
|
if (!(checker && checker instanceof Type)) continue; |
|
result[key] = checker.as(input[key as keyof typeof input]); |
|
} |
|
return result; |
|
} |
|
|
|
override validate( |
|
input: unknown, |
|
ctx: Context<Type.infer<T>>, |
|
): ValidationResult<Type.infer<T>> { |
|
if (typeof input !== "object" || input === null) { |
|
return ctx.err`Expected an object of type ${this}, got ${input}`; |
|
} |
|
const result: any = {}; |
|
for (const key in this.schema) { |
|
const checker = this.schema[key]; |
|
if (!(checker && checker instanceof Type)) continue; |
|
const sub = ctx.with(key); |
|
const res = checker.validate(input[key as keyof typeof input], sub); |
|
if (!res.success) { |
|
const error = new TypeError( |
|
`Failed to validate property ${key} (type: ${checker}) of ${this}`, |
|
{ cause: res.error }, |
|
); |
|
return ctx.err(error, sub.path); |
|
} |
|
result[key] = res.value; |
|
} |
|
return ctx.ok(result); |
|
} |
|
|
|
override toJSON(): object { |
|
const result: any = {}; |
|
for (const key in this.schema) { |
|
const checker = this.schema[key]; |
|
if (!(checker && checker instanceof Type)) continue; |
|
result[key] = checker.toJSON(); |
|
} |
|
return result; |
|
} |
|
|
|
override toString(): string { |
|
if (this.name !== "object") return this.name; |
|
// @ts-expect-error readonly property |
|
return this.name = ObjectType.render(this.schema); |
|
} |
|
} |
|
|
|
export function object<const T extends Record<string, Type<any>>>( |
|
schema: T, |
|
): ObjectType<T> { |
|
return new ObjectType(schema); |
|
} |
|
|
|
export interface RecordType<K extends PropertyKey, V> { |
|
readonly constructor: typeof RecordType<K, V>; |
|
} |
|
|
|
export class RecordType<K extends PropertyKey, V> extends Type<Record<K, V>> { |
|
static override readonly name = "Record<K, V>"; |
|
|
|
constructor( |
|
readonly keyType: Type<K>, |
|
readonly valueType: Type<V>, |
|
info?: Partial<TypeInfoState<Record<K, V>>>, |
|
) { |
|
super(`Record<${keyType}, ${valueType}>`, info); |
|
} |
|
|
|
override is(input: unknown): input is Record<K, V> { |
|
if (typeof input !== "object" || input === null) return false; |
|
for (const key in input) { |
|
if (!this.keyType.is(key)) return false; |
|
if (!this.valueType.is((input as any)[key])) return false; |
|
} |
|
return true; |
|
} |
|
|
|
override as(input: unknown): Record<K, V> { |
|
if (typeof input !== "object" || input === null) { |
|
throw new TypeError(`Expected object, got ${typeof input}`); |
|
} |
|
const result = { __proto__: null } as unknown as Record<K, V>; |
|
for (const key in input) { |
|
const k = this.keyType.as(key); |
|
const v = this.valueType.as((input as any)[k]); |
|
result[k] = v; |
|
} |
|
return result; |
|
} |
|
|
|
override validate( |
|
input: unknown, |
|
ctx: Context<Record<K, V>>, |
|
): ValidationResult<Record<K, V>> { |
|
if (typeof input !== "object" || input === null) { |
|
return ctx.err(`Expected object, got ${typeof input}`); |
|
} |
|
const result: any = {}; |
|
for (const key in input) { |
|
const k = this.keyType.validate(key, ctx as Context); |
|
if (!k.success) return ctx.err(k.error); |
|
const v = this.valueType.validate( |
|
(input as any)[k.value], |
|
ctx.with(k.value) as Context, |
|
); |
|
if (!v.success) return ctx.err(v.error); |
|
result[k.value] = v.value; |
|
} |
|
return ctx.ok(result); |
|
} |
|
} |
|
|
|
export function record<K extends PropertyKey, V>( |
|
keyType: Type<K>, |
|
valueType: Type<V>, |
|
): RecordType<K, V> { |
|
return new RecordType(keyType, valueType); |
|
} |
|
|
|
export type Tuple<T = any> = readonly [] | readonly [T, ...T[]]; |
|
|
|
export interface TupleType<T extends Tuple<Type<any>>> { |
|
readonly constructor: typeof TupleType<T>; |
|
} |
|
|
|
export class TupleType<const T extends Tuple<Type<any>>> |
|
extends Type<InferTuple<T>> { |
|
static override readonly name = "Tuple<T>"; |
|
|
|
constructor( |
|
protected types: T, |
|
info?: Partial<TypeInfoState<InferTuple<T>>>, |
|
) { |
|
super(`Tuple<[${types}]>`, { ...info, validator: isArray }); |
|
} |
|
|
|
override is(input: unknown): input is InferTuple<T> { |
|
if (!super.is(input)) return false; |
|
if (input.length !== this.types.length) return false; |
|
for (let i = 0; i < this.types.length; i++) { |
|
if (!this.types[i].is(input[i])) return false; |
|
} |
|
return true; |
|
} |
|
|
|
override as(input: unknown): InferTuple<T> { |
|
if (!super.is(input)) { |
|
throw new TypeError(`Expected a tuple (array), got ${typeof input}`); |
|
} |
|
const result = [] as unknown as InferTuple<T>; |
|
for (let i = 0; i < this.types.length; i++) { |
|
result[i] = this.types[i].as(input[i]); |
|
} |
|
return result; |
|
} |
|
|
|
override validate( |
|
input: unknown, |
|
ctx: Context, |
|
): ValidationResult<InferTuple<T>> { |
|
if (!isArray(input)) { |
|
return ctx.err(`Expected a tuple (array), got ${typeof input}`); |
|
} |
|
const result: any[] = []; |
|
for (let i = 0; i < this.types.length; i++) { |
|
const subContext = ctx.with(String(i)); |
|
const res = this.types[i].validate(input[i], subContext); |
|
if (!res.success) { |
|
return ctx.err(`Error at index ${i}: ${res.error}`); |
|
} |
|
result.push(res.value); |
|
} |
|
return ctx.ok(result); |
|
} |
|
} |
|
|
|
export function tuple<const T extends Tuple<Type<any>>>( |
|
...types: T |
|
): TupleType<T> { |
|
return new TupleType(types); |
|
} |
|
|
|
export interface ArrayType<T> { |
|
readonly constructor: typeof ArrayType<T>; |
|
} |
|
|
|
export class ArrayType<T> extends Type<T[]> { |
|
static override readonly name = "Array<T>"; |
|
|
|
constructor( |
|
protected elementType: Type<T> = any, |
|
info?: Partial<TypeInfoState<T[]>>, |
|
) { |
|
const et = elementType.is.bind(elementType); |
|
const validator = (x: unknown): x is T[] => isArray(x, et); |
|
super(`Array<${elementType}>`, { ...info, validator }); |
|
} |
|
|
|
override as(input: unknown): T[] { |
|
if (!isArray(input)) { |
|
throw new TypeError(`Expected array, got ${typeof input}`); |
|
} |
|
return input.map((el) => this.elementType.as(el)); |
|
} |
|
|
|
override validate(input: unknown, ctx: Context): ValidationResult<T[]> { |
|
if (!isArray(input)) { |
|
return ctx.err(`Expected array, got ${typeof input}`); |
|
} |
|
const result: T[] = []; |
|
for (let i = 0; i < input.length; i++) { |
|
const subContext = ctx.with(String(i)); |
|
const res = this.elementType.validate(input[i], subContext); |
|
if (!res.success) { |
|
return ctx.err`Error at index ${i}: ${res.error}`; |
|
} |
|
result.push(res.value); |
|
} |
|
return ctx.ok(result); |
|
} |
|
} |
|
|
|
export interface ArrayTypeFactory extends ArrayType<any> { |
|
<T>(elementType: Type<T>): ArrayType<T>; |
|
|
|
new <T>(elementType: Type<T>): ArrayType<T>; |
|
} |
|
|
|
const arrayType = new ArrayType(); |
|
|
|
export const array: ArrayTypeFactory = ObjectSetPrototypeOf( |
|
function array<T>(elementType: Type<T>): ArrayType<T> { |
|
return new ArrayType(elementType); |
|
}, |
|
arrayType, |
|
); |
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------- |
|
// Registry and Refs |
|
// ---------------------------------------------------------------------------- |
|
|
|
export class RefType< |
|
R extends Registry, |
|
K extends strings | string & keyof R["types"] = string & keyof R["types"], |
|
T extends R["types"][keyof R["types"]] = K extends keyof R["types"] |
|
? R["types"][K] |
|
: R["types"][keyof R["types"]], |
|
V = Type.Infer<T>, |
|
> extends Type<V> { |
|
constructor( |
|
protected refName: K, |
|
protected registry: R, |
|
) { |
|
super(refName); |
|
} |
|
|
|
override is(input: unknown): input is V { |
|
return this.registry.get(this.refName).is(input); |
|
} |
|
|
|
override as(input: unknown): V { |
|
return this.registry.get(this.refName).as(input); |
|
} |
|
|
|
override validate(input: unknown, ctx: Context): ValidationResult<V> { |
|
return this.registry.get(this.refName).validate(input, ctx); |
|
} |
|
} |
|
|
|
export function ref< |
|
R extends Registry, |
|
K extends strings | string & keyof R["types"] = string & keyof R["types"], |
|
T extends R["types"][keyof R["types"]] = K extends keyof R["types"] |
|
? R["types"][K] |
|
: R["types"][keyof R["types"]], |
|
V = Type.Infer<T>, |
|
>(name: K, registry: R): RefType<R, K, T, V> { |
|
return new RefType(name, registry); |
|
} |
|
|
|
export interface RegistryTypes { |
|
[name: string]: Type<any> | ((...args: any[]) => Type<any>) | {}; |
|
} |
|
|
|
export class Registry< |
|
Name extends string = string, |
|
const Types extends RegistryTypes = RegistryTypes, |
|
> { |
|
static create< |
|
Name extends string, |
|
const Types extends RegistryTypes, |
|
>(name: Name, types: Types): Registry<Name, Types> { |
|
return new Registry(name, { ...types }); |
|
} |
|
|
|
static from< |
|
Name extends string, |
|
NewName extends string = Name, |
|
const Types extends RegistryTypes = RegistryTypes, |
|
>(other: Registry<Name, Types>, name?: NewName): Registry<NewName, Types> { |
|
return new Registry( |
|
(name ?? other.name) as NewName, |
|
{ ...other.types }, |
|
); |
|
} |
|
|
|
constructor( |
|
readonly name: Name, |
|
readonly types: Types, |
|
) { |
|
this.types = { ...types }; |
|
} |
|
|
|
register<K extends string, T extends Type<any>>( |
|
key: K, |
|
lazyType: (this: this, registry: this) => T, |
|
): Registry<Name, Types & { [P in K]: T }>; |
|
register<K extends string, T extends Type<any>>( |
|
key: K, |
|
type: T | Type<Type.Infer<T>>, |
|
): Registry<Name, Types & { [P in K]: T }>; |
|
register<K extends string, T extends Type<any>>( |
|
key: K, |
|
type: T | ((this: this, registry: this) => T), |
|
): Registry<Name, Types & { [P in K]: T }>; |
|
register<K extends string, T extends Type<any>>( |
|
key: K, |
|
type: T | ((this: this, registry: this) => T), |
|
): Registry<Name, Types & { [P in K]: T }> { |
|
this.define(key, type); |
|
return this as any; |
|
} |
|
|
|
define<K extends string, T extends Type<any>>( |
|
key: K, |
|
lazyType: (this: this, registry: this) => T, |
|
): asserts this is this & Registry<Name, Types & { [P in K]: T }>; |
|
define<K extends string, T extends Type<any>>( |
|
key: K, |
|
type: T | Type<Type.Infer<T>>, |
|
): asserts this is this & Registry<Name, Types & { [P in K]: T }>; |
|
define<K extends string, T extends Type<any>>( |
|
key: K, |
|
type: T | ((this: this, registry: this) => T), |
|
): asserts this is this & Registry<Name, Types & { [P in K]: T }>; |
|
define<K extends string, T extends Type<any>>( |
|
key: K, |
|
type: T | ((this: this, registry: this) => T), |
|
): asserts this is this & Registry<Name, Types & { [P in K]: T }> { |
|
const k = key as K & keyof Types; |
|
if (isFunction(type) && !(type instanceof Type)) { |
|
this.types[k] = FunctionPrototypeCall(type, this, this) as never; |
|
} else { |
|
this.types[k] = type as never; |
|
} |
|
} |
|
|
|
get<K extends string = string & keyof Types>( |
|
key: K, |
|
): K extends keyof Types ? Types[K] : never; |
|
get<K extends keyof Types>(key: K): Types[K]; |
|
get(key: string): any { |
|
const type = this.types[key]; |
|
if (!(type && isFunction(type))) { |
|
throw new Error(`Type ${String(key)} not found in registry ${this.name}`); |
|
} |
|
return type; |
|
} |
|
} |
|
|
|
export function registry< |
|
Name extends string, |
|
const Types extends RegistryTypes, |
|
>(name: Name, types: Types): Registry<Name, Types> { |
|
return new Registry(name, types); |
|
} |
|
|
|
export type InferTuple<T, F = T> = T extends readonly [] ? [] |
|
: T extends readonly [infer A, ...infer B] |
|
? B extends readonly [] ? [Infer<A>] |
|
: [Infer<A>, ...InferTuple<B, F>] |
|
: F extends readonly unknown[] ? F |
|
: []; |
|
|
|
export type Infer<T, F = T> = T extends { readonly _typeof: infer U } ? U |
|
: T extends readonly unknown[] ? InferTuple<T> |
|
: T extends (this: infer This, ...args: infer Args) => infer Return |
|
? (this: Infer<This>, ...args: InferTuple<Args>) => Infer<Return> |
|
: T extends (...args: infer Args) => infer Return |
|
? (...args: InferTuple<Args>) => Infer<Return> |
|
: T extends Record<PropertyKey, any> ? { [P in keyof T]: Infer<T[P]> } |
|
: T extends Map<infer K extends Type<any>, infer V extends Type<any>> |
|
? Map<Infer<K>, Infer<V>> |
|
: T extends Set<infer V extends Type<any>> ? Set<Infer<V>> |
|
: T extends WeakMap<infer K extends Type<any>, infer V extends Type<any>> |
|
? WeakMap<Infer<K, WeakKey>, Infer<V>> |
|
: T extends WeakSet<infer V extends Type<any>> ? WeakSet<Infer<V, WeakKey>> |
|
: T extends WeakRef<infer V extends Type<any>> ? WeakRef<Infer<V, WeakKey>> |
|
: F; |
|
|
|
export type { Infer as infer }; |
|
|
|
export declare namespace Type { |
|
export { defaultInspectOptions, defaultRenderOptions }; |
|
// export { |
|
// fn as function, |
|
// _null as null, |
|
// _undefined as undefined, |
|
// _void as void, |
|
// any, |
|
// array, |
|
// asyncFunction, |
|
// asyncGenerator, |
|
// asyncGeneratorFunction, |
|
// asyncIterable, |
|
// asyncIterableIterator, |
|
// asyncIterator, |
|
// bigint, |
|
// boolean, |
|
// date, |
|
// error, |
|
// generator, |
|
// generatorFunction, |
|
// iterable, |
|
// iterableIterator, |
|
// iterator, |
|
// map, |
|
// number, |
|
// object, |
|
// primitive, |
|
// record, |
|
// regExp, |
|
// set, |
|
// string, |
|
// symbol, |
|
// tuple, |
|
// unknown, |
|
// weakMap, |
|
// weakRef, |
|
// weakSet, |
|
// }; |
|
export type { Infer, Infer as infer }; |
|
} |