Last active
March 14, 2025 21:12
-
-
Save nberlette/cddab1da4e2a941bc8f7df7b32c11c44 to your computer and use it in GitHub Desktop.
typescript runtime type validation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// deno-lint-ignore-file no-explicit-any no-namespace | |
import { inspect, type InspectOptions } from "node:util"; | |
// #region Validation Types | |
export type Err<T = never> = { | |
success: false; | |
error: string | ValidationError<T>; | |
}; | |
export type Ok<T> = { | |
success: true; | |
data: T; | |
}; | |
export type Result<T> = Err<T> | Ok<T>; | |
type PathPart = string | number; | |
export type ValidationPath = PathPart | readonly PathPart[]; | |
export interface ValidationErrorOptions<T extends I = any, I = unknown> extends ErrorOptions { | |
/** The path to the value that caused the error. */ | |
path?: ValidationPath; | |
/** The validator that threw the error. */ | |
validator?: Type<T, I>; | |
/** Human-readable representation of the Type's expected type. */ | |
expected?: string; | |
/** The value that caused the error. */ | |
value?: T; | |
} | |
export class ValidationError<T extends I = any, I = unknown> extends TypeError { | |
override readonly message: string; | |
readonly options?: ValidationErrorOptions<T, I>; | |
constructor(message: string, options?: ValidationErrorOptions<T, I>); | |
constructor(cause: ValidationError<T, I>, options?: Omit<ValidationErrorOptions<T, I>, "cause">); | |
constructor(error: string | ValidationError<T, any>, options?: ValidationErrorOptions<T, I>); | |
constructor( | |
message: string | ValidationError<T, I>, | |
options?: ValidationErrorOptions<T, I>, | |
) { | |
let cause: unknown; | |
if (typeof message !== "string") { | |
cause = message; | |
({ message, options } = { options, ...message }); | |
} else { | |
({ cause } = options ??= {} as ValidationErrorOptions<T, I>); | |
message ??= "Validation failed"; | |
} | |
super(message, { cause }); | |
this.message = message; | |
this.options = options; | |
this.name = "ValidationError"; | |
} | |
} | |
// #endregion Validation Types | |
// #region Type Definitions | |
export type InferType<V> = V extends Type<infer T> ? T : never; | |
export interface PrimitiveTypeMap { | |
string: string; | |
number: number; | |
boolean: boolean; | |
bigint: bigint; | |
symbol: symbol; | |
} | |
export interface NullablePrimitiveTypeMap extends PrimitiveTypeMap { | |
undefined: undefined; | |
null: null; | |
} | |
export interface TypeMap<A = any, B = any> extends NullablePrimitiveTypeMap { | |
object: object; | |
// function: Function; | |
function: ToFunctionType<A, B>; | |
} | |
export type PrimitiveTypeName = keyof PrimitiveTypeMap; | |
export type NullablePrimitiveTypeName = keyof NullablePrimitiveTypeMap; | |
export type NullablePrimitive<K extends NullablePrimitiveTypeName = NullablePrimitiveTypeName> = NullablePrimitiveTypeMap[K]; | |
export type TypeName = string & keyof TypeMap; | |
type IsNever<U, T = true, F = false> = [U] extends [never] ? T : F; | |
type IsAny<U, T = true, F = false> = boolean extends ( | |
U extends never ? true : false | |
) ? T : F; | |
type IsAnyOrNever<U, T = true, F = false> = IsNever<U, T, IsAny<U, T, F>>; | |
type ToFunctionType<A, B> = | |
| IsNever<A> extends true | |
? IsNever<B> extends true | |
? () => void | |
: () => B | |
: IsNever<B> extends true | |
? A extends readonly unknown[] | |
? (...args: A) => void | |
: (...args: A[]) => void | |
: A extends readonly unknown[] | |
? (...args: A) => B | |
: (...args: A[]) => B; | |
// Object Types | |
type RequiredKeys<T> = { | |
[K in keyof T]-?: undefined extends InferType<T[K]> ? never : K; | |
}[keyof T]; | |
type OptionalKeys<T> = { | |
[K in keyof T]-?: undefined extends InferType<T[K]> ? K : never; | |
}[keyof T]; | |
// deno-fmt-ignore | |
export type InferObjectType< | |
T extends { [key: PropertyKey]: Any | undefined }, | |
> = { [K in RequiredKeys<T>]-?: InferType<T[K]> } | |
& { [K in OptionalKeys<T>]+?: InferType<T[K]> }; | |
// #endregion Type Definitions | |
// #region Constants and Metadata | |
export interface Metadata<T> { | |
name: string; | |
message?: string; | |
format?: string; | |
test?: (data: any) => boolean; | |
coerce?: (data: any) => T; | |
convert?: (data: any) => T; | |
} | |
export type ResolvedMetadata<T> = | |
& Readonly<Required<Omit<Metadata<T>, "message" | "format">>> | |
& Pick<Metadata<T>, "message" | "format">; | |
/** | |
* Well-known symbol used to store internal metadata on an instance of the | |
* abstract {@linkcode Type} class. This is used to store information, | |
* configuration, and other data specific to a particular validator subclass, | |
* to allow for more accurate and informative error reporting. | |
* | |
* @category Symbols | |
* @internal | |
*/ | |
export const _metadata: unique symbol = Symbol("Type.#metadata"); | |
export type _metadata = typeof _metadata; | |
export const _type: unique symbol = Symbol("Type.#type"); | |
export type _type = typeof _type; | |
export const _input: unique symbol = Symbol("Type.#input"); | |
export type _input = typeof _input; | |
// #endregion Constants and Metadata | |
// #region Type (Abstract Base Class) | |
export interface Type<T extends I, I = unknown> { | |
readonly [_input]: I; | |
readonly [_type]: T; | |
(input: I): T; | |
is(data: I): data is T; | |
assert(data: I): asserts data is T; | |
coerce(data: unknown): T; | |
convert(data: I): T; | |
} | |
export abstract class Type<T extends I, I = unknown> extends Function { | |
static of(data: unknown): TypeName { | |
return data === null ? "null" : typeof data; | |
} | |
override readonly name: string; | |
#metadata: Metadata<T>; | |
constructor(meta: Metadata<T>); | |
constructor(name: string, meta?: Partial<Metadata<T>>); | |
constructor( | |
name: string, | |
test: (it: I) => it is T, | |
convert?: (input: I) => T, | |
); | |
constructor( | |
name: string | Metadata<T>, | |
test?: ((it: I) => it is T) | Partial<Metadata<T>>, | |
convert?: (input: I) => T, | |
) { | |
super("...args", "return this.convert(...args)"); | |
let meta = { name: "unknown" } as Metadata<T>; | |
if (typeof name === "string") { | |
meta = { ...meta, name }; | |
} else if (name != null && typeof name === "object") { | |
meta = { ...meta, ...name }; | |
} | |
if (typeof test === "function") { | |
meta = { ...meta, test }; | |
} else if (test != null && typeof test === "object") { | |
meta = { ...meta, ...test }; | |
} | |
if (typeof convert === "function") { | |
meta = { ...meta, convert }; | |
} | |
if (!meta.name || typeof meta.name !== "string") { | |
throw new TypeError("Validator name must be a string"); | |
} | |
this.name = meta.name; | |
this.#metadata = { | |
coerce: (data) => { | |
if (meta.coerce) return meta.coerce(data); | |
this.assert(data); | |
return data; | |
}, | |
convert: (data) => { | |
if (meta.convert) return meta.convert(data); | |
if (meta.coerce) return meta.coerce(data); | |
this.assert(data); | |
return data; | |
}, | |
test: (data) => this.validate(data).success, | |
...meta, | |
}; | |
const cache = new WeakMap(); | |
return new Proxy(this, { | |
apply: (target, _, args) => { | |
return target.convert.call(this, ...args as [I]); | |
}, | |
get: (t, p) => { | |
const v = t[p as keyof this]; | |
if (typeof v === "function") { | |
if (cache.has(v)) return cache.get(v); | |
const bound = v.bind(t); | |
Object.defineProperty(bound, "name", { value: v.name }); | |
cache.set(v, bound); | |
return bound; | |
} else { | |
return v; | |
} | |
}, | |
}); | |
} | |
abstract validate(data: I): Result<T>; | |
protected get [_metadata](): ResolvedMetadata<T> { | |
return this.#metadata as ResolvedMetadata<T>; | |
} | |
is(value: I): value is T { | |
return this.validate(value).success; | |
} | |
assert(value: I): asserts value is T { | |
const result = this.validate(value); | |
if (!result.success) { | |
const error = new ValidationError(result.error, { | |
validator: this, | |
value, | |
}); | |
Error.captureStackTrace?.(error, this.assert); | |
error.stack; // force stack to be generated | |
throw error; | |
} | |
} | |
coerce(value: unknown): T { | |
return this[_metadata].coerce.call(this, value); | |
} | |
convert(value: I): T { | |
return this[_metadata].convert.call(this, value); | |
} | |
inspect(options?: InspectOptions): string; | |
inspect(depth?: number, options?: InspectOptions): string; | |
inspect(depth?: number | InspectOptions, options?: InspectOptions) { | |
depth = typeof depth === "number" ? depth : options?.depth ?? 2; | |
options = (typeof depth === "number" ? options : depth ?? options) ?? {}; | |
const { name } = this[_metadata]; | |
const s = (v: any, t: string) => (options as any).stylize?.(v, t) ?? v; | |
if (depth <= 0) return s(`[${name}]`, "special"); | |
return `${name} ${inspect({ ...this }, { ...options, depth })}`; | |
} | |
override toString(): string { | |
return this[_metadata].name; | |
} | |
} | |
export declare namespace Type { | |
export { _metadata as metadata, _type as type }; | |
} | |
export namespace Type { | |
Type.metadata = _metadata; | |
Type.type = _type; | |
} | |
// #endregion Type (Abstract Base Class) | |
// #region Abstracts | |
export class Any extends Type<any, any> { | |
constructor() { | |
super("any"); | |
} | |
validate(data: any): Result<any> { | |
return { success: true, data }; | |
} | |
} | |
export class Unknown extends Type<unknown, unknown> { | |
constructor() { | |
super("unknown"); | |
} | |
validate(data: unknown): Result<unknown> { | |
return { success: true, data }; | |
} | |
} | |
export class Never extends Type<never, unknown> { | |
constructor() { | |
super("never"); | |
} | |
validate(_: unknown): Result<never> { | |
return { | |
success: false, | |
error: "Never type cannot be validated", | |
}; | |
} | |
} | |
// #endregion Abstracts | |
// #region Primitive | |
export class PrimitiveType<T extends TypeMap[K], K extends TypeName = PrimitiveTypeName> extends Type<T> { | |
constructor( | |
protected type: K, | |
meta?: Partial<Metadata<T>>, | |
) { | |
super(type, meta); | |
} | |
validate(value: unknown): Result<T> { | |
const type = Type.of(value); | |
if (type === this.type) { | |
const data = value as T; | |
return { success: true, data }; | |
} else { | |
return { | |
success: false, | |
error: `Expected a value of type ${this.type}, but received ${type}`, | |
}; | |
} | |
} | |
} | |
export class StringType extends PrimitiveType<string, "string"> { | |
constructor() { | |
super("string", { coerce: String }); | |
} | |
} | |
export class NumberType extends PrimitiveType<number, "number"> { | |
constructor() { | |
super("number", { coerce: Number }); | |
} | |
} | |
export class BigIntType extends PrimitiveType<bigint, "bigint"> { | |
constructor() { | |
super("bigint", { coerce: BigInt }); | |
} | |
} | |
export class BooleanType extends PrimitiveType<boolean, "boolean"> { | |
constructor() { | |
super("boolean", { coerce: Boolean }); | |
} | |
} | |
export class SymbolType extends PrimitiveType<symbol, "symbol"> { | |
constructor() { | |
super("symbol", { coerce: Symbol }); | |
} | |
} | |
const Undefined: (v?: unknown) => undefined = (v) => void v; | |
export class UndefinedType extends PrimitiveType<undefined, "undefined"> { | |
constructor() { | |
super("undefined", { coerce: Undefined }); | |
} | |
} | |
// #endregion Primitive | |
// #region Object | |
export class ObjectType<T extends object = object> extends PrimitiveType<T, "object"> { | |
constructor() { | |
super("object", { coerce: Object }); | |
} | |
validate(data: unknown): Result<T> { | |
if (typeof data === "object" && data !== null && !Array.isArray(data)) { | |
return { success: true, data: data as T }; | |
} else { | |
return super.validate(data); | |
} | |
} | |
} | |
export class SchemaType< | |
const T extends { [key: PropertyKey]: Any | undefined }, | |
> extends Type<InferObjectType<T>> { | |
constructor(protected readonly shape: T) { | |
super(Object.entries(shape).reduce((acc, [k, v], i, a) => { | |
return `${acc || "{"}\n ${k}: ${v};${i < a.length - 1 ? "" : "\n}"}`; | |
}, "")); | |
} | |
validate(data: unknown): Result<InferObjectType<T>> { | |
if (typeof data !== "object" || data === null || Array.isArray(data)) { | |
return { | |
success: false, | |
error: `Expected object, but received ${typeof data}`, | |
}; | |
} | |
const result = {} as InferObjectType<T>; | |
for (const key in this.shape) { | |
const propValidator = this.shape[key]; | |
const value = (data as any)[key]; | |
if (typeof value === "undefined") { | |
const undef = propValidator?.validate(undefined); | |
if (!undef?.success) { | |
return { | |
success: false, | |
error: `Missing required key "${key}"`, | |
}; | |
} else if (typeof undef?.data !== "undefined") { | |
(result as Record<PropertyKey, any>)[key] = undef.data; | |
} | |
} else { | |
const result = propValidator?.validate(value); | |
if (!result?.success) { | |
return { | |
success: false, | |
error: `Invalid value for key "${key}": ${result?.error}`, | |
}; | |
} else { | |
(result as Record<PropertyKey, any>)[key] = result.data; | |
} | |
} | |
} | |
return { success: true, data: result }; | |
} | |
} | |
export class RecordType<K extends PropertyKeyType, V extends Any> extends Type<Record<InferType<K>, InferType<V>>> { | |
constructor(protected keyType: K, protected valueType: V) { | |
super(`Record<${keyType}, ${valueType}>`); | |
} | |
validate(data: unknown): Result<Record<InferType<K>, InferType<V>>> { | |
if (typeof data !== "object" || data === null || Array.isArray(data)) { | |
return { | |
success: false, | |
error: `Expected object, but received ${typeof data}`, | |
}; | |
} | |
const result = {} as Record<PropertyKey, InferType<V>>; | |
for (const key in data) { | |
const keyResult = this.keyType.validate(key); | |
if (!keyResult.success) { | |
return { | |
success: false, | |
error: `Invalid key: ${keyResult.error}`, | |
}; | |
} | |
const value = data[key as keyof typeof data]; | |
const valueResult = this.valueType.validate(value); | |
if (!valueResult.success) { | |
return { | |
success: false, | |
error: `Invalid value at key "${key}": ${valueResult.error}`, | |
}; | |
} | |
result[keyResult.data] = valueResult.data; | |
} | |
return { success: true, data: result }; | |
} | |
} | |
// #endregion Object | |
// #region Array | |
export class ArrayType<T extends Any> extends Type<InferType<T>[]> { | |
constructor(protected valueType: T) { | |
super(`Array<${valueType}>`); | |
} | |
validate(data: unknown): Result<InferType<T>[]> { | |
if (!Array.isArray(data)) { | |
return { | |
success: false, | |
error: `Expected ${this}, but received ${Type.of(data)}`, | |
}; | |
} | |
const results: InferType<T>[] = []; | |
for (let i = 0; i < data.length; i++) { | |
const item = data[i]; | |
const result = this.valueType.validate(item); | |
if (!result.success) { | |
return { | |
success: false, | |
error: `Invalid value at index ${i}: ${result.error}`, | |
}; | |
} else { | |
results.push(result.data); | |
} | |
} | |
return { success: true, data: results }; | |
} | |
} | |
// #endregion Array | |
// #region Union | |
export class UnionType<const U extends readonly Any[]> extends Type< | |
InferType<U[number]> | |
> { | |
constructor(protected readonly subtypes: U) { | |
const name = subtypes.map((t) => { | |
if (t instanceof UnionType) return `(${t.name})`; | |
if (t instanceof IntersectionType) return `(${t.name})`; | |
return t.name; | |
}).join(" | "); | |
super(name); | |
} | |
validate(data: unknown): Result<InferType<U[number]>> { | |
for (const subtype of this.subtypes) { | |
const result = subtype.validate(data); | |
if (result.success) return result; | |
} | |
return { | |
success: false, | |
error: `Expected ${this.toString()}, but received ${Type.of(data)}`, | |
}; | |
} | |
} | |
// #endregion Union | |
// #region Intersection | |
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ( | |
k: infer I, | |
) => void | |
? I | |
: never; | |
export class IntersectionType<const I extends readonly Any[]> extends Type< | |
UnionToIntersection<InferType<I[number]>> | |
> { | |
constructor(protected readonly subtypes: I) { | |
const name = subtypes.map((t) => { | |
if (t instanceof UnionType) return `(${t.name})`; | |
if (t instanceof IntersectionType) return `(${t.name})`; | |
return t.name; | |
}).join(" & "); | |
super(name); | |
} | |
validate(data: unknown): Result<UnionToIntersection<InferType<I[number]>>> { | |
for (const subtype of this.subtypes) { | |
const result = subtype.validate(data); | |
if (!result.success) return result; | |
} | |
return { success: true, data: data as UnionToIntersection<InferType<I[number]>> }; | |
} | |
} | |
// #endregion Intersection | |
// #region Literal | |
export class LiteralType<T extends NullablePrimitive> extends Type<T> { | |
constructor( | |
readonly value: T, | |
meta?: Partial<Metadata<T>>, | |
) { | |
super(inspect(value), meta); | |
} | |
validate(data: unknown): Result<T> { | |
if (this.value === data) { | |
return { success: true, data: this.value }; | |
} else { | |
return { | |
success: false, | |
error: `Expected literal ${this.inspect()}, but received ${data}`, | |
}; | |
} | |
} | |
override inspect(): string { | |
return inspect(this.value); | |
} | |
} | |
export class NullType extends LiteralType<null> { | |
constructor() { | |
super(null, { coerce: () => null }); | |
} | |
} | |
// #endregion Literal | |
// #region InstanceOf | |
export class InstanceOf<T extends abstract new (...args: any) => any> extends Type< | |
InstanceType<T> | |
> { | |
constructor(protected ctor: T) { | |
super(`InstanceOf<${ctor.name}>`); | |
} | |
validate(data: unknown): Result<InstanceType<T>> { | |
if (data instanceof this.ctor) { | |
return { success: true, data: data as InstanceType<T> }; | |
} else { | |
return { | |
success: false, | |
error: `Expected instance of ${this.ctor.name}, but received ${data}`, | |
}; | |
} | |
} | |
} | |
// #endregion InstanceOf | |
// #region Optional + Partial | |
export class Optional<T extends Any> extends UnionType<[T, UndefinedType]> { | |
constructor(protected type: T) { | |
super([type, undefined_]); | |
} | |
} | |
export class PartialType<const T extends { [key: PropertyKey]: Any }> extends Type< | |
Partial<InferObjectType<T>> | |
> { | |
/** | |
* Creates a new `Partial` validator for the given shape, making all keys in | |
* the object optional. | |
* | |
* If the `exact` flag is set to `true`, then the type will treat missing | |
* properties as distinct from those with `undefined` values (i.e. it will | |
* only allow its keys to be omitted, and will error if the key is present | |
* with a value of `undefined`). | |
* | |
* The `exact` flag will also cause any additional properties not present in | |
* the shape to error. | |
* | |
* @param shape The shape of the object to validate. | |
* @param [exact=false] Whether to treat missing properties as distinct from | |
* those with `undefined` values, and to error on additional properties. | |
*/ | |
constructor(protected shape: T, protected readonly exact: boolean = false) { | |
super(`Partial<{${ | |
Object.entries(shape).reduce((acc, [k, v]) => { | |
return `${acc} ${k}?: ${v}${exact ? "" : " | undefined"};\n`; | |
}, "\n") | |
}}>`); | |
} | |
validate(data: unknown): Result<Partial<InferObjectType<T>>> { | |
if (typeof data !== "object" || data === null || Array.isArray(data)) { | |
return { | |
success: false, | |
error: `Expected object, but received ${typeof data}`, | |
}; | |
} | |
const result = {} as InferObjectType<T>; | |
for (const key in this.shape) { | |
const propValidator = this.shape[key]; | |
const value = (data as any)[key]; | |
if (typeof value === "undefined") { | |
if (this.exact && !(key in data)) { | |
continue; // skip undefined values for exact types | |
} | |
} else { | |
const result = propValidator?.validate(value); | |
if (!result?.success) { | |
return { | |
success: false, | |
error: `Invalid value for key "${key}": ${result?.error}`, | |
}; | |
} else { | |
(result as Record<PropertyKey, any>)[key] = result.data; | |
} | |
} | |
} | |
return { success: true, data: result }; | |
} | |
} | |
// #endregion Partial | |
// #region PropertyKey | |
export class PropertyKeyType extends UnionType<[StringType, NumberType, SymbolType]> { | |
constructor() { | |
super([string, number, symbol]); | |
} | |
} | |
// #endregion PropertyKey | |
export const any: Any = new Any(); | |
export const unknown: Unknown = new Unknown(); | |
export const never: Never = new Never(); | |
export const string: StringType = new StringType(); | |
export const number: NumberType = new NumberType(); | |
export const bigint: BigIntType = new BigIntType(); | |
export const symbol: SymbolType = new SymbolType(); | |
export const boolean: BooleanType = new BooleanType(); | |
export const propertyKey: PropertyKeyType = new PropertyKeyType(); | |
const undefined_: UndefinedType = new UndefinedType(); | |
const null_: NullType = new NullType(); | |
export const nullish = union(null_, undefined_); | |
export { null_ as null, undefined_ as undefined }; | |
export function primitive<K extends NullablePrimitiveTypeName>( | |
typeName: K, | |
): PrimitiveType<TypeMap[K], K> { | |
return new PrimitiveType(typeName); | |
} | |
export function union<const U extends readonly Type<T>[], T = unknown>( | |
...subtypes: U | |
): UnionType<U> { | |
return new UnionType(subtypes); | |
} | |
export function intersection<const I extends readonly Type<T>[], T = unknown>( | |
...subtypes: I | |
): IntersectionType<I> { | |
return new IntersectionType(subtypes); | |
} | |
export function literal<T extends NullablePrimitive>(value: T): LiteralType<T> { | |
return new LiteralType(value); | |
} | |
export function instanceOf<T extends abstract new (...args: any) => any>( | |
constructor: T, | |
): InstanceOf<T> { | |
return new InstanceOf(constructor); | |
} | |
export function array<T extends Any>(valueType: T): ArrayType<T> { | |
return new ArrayType(valueType); | |
} | |
export function schema< | |
const T extends { readonly [key: string]: Any }, | |
>(shape: T): SchemaType<T> { | |
return new SchemaType(shape); | |
} | |
export function optional<T extends Any>( | |
validator: T, | |
): Optional<T> { | |
return new Optional(validator); | |
} | |
export function partial< | |
const T extends { [key: PropertyKey]: Any }, | |
>(shape: T, exact?: boolean): PartialType<T> { | |
return new PartialType(shape, exact); | |
} | |
export function record<K extends PropertyKeyType, V extends Any>( | |
keyType: K, | |
valueType: V, | |
): RecordType<K, V> { | |
return new RecordType(keyType, valueType); | |
} | |
export function object<T extends object = object>(): ObjectType<T> { | |
return new ObjectType(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
object, | |
string, | |
number, | |
boolean, | |
optional, | |
record, | |
union, | |
any, | |
partial, | |
type Infer, | |
} from "./runtime_types.ts"; | |
const config = object({ | |
name: string, | |
version: string, | |
description: optional(string), | |
metadata: optional(record(string, any)), | |
settings: partial(record(string, union(string, number, boolean))), | |
}); | |
type config = Infer<typeof config>; | |
// ^? type config = { | |
// name: string; | |
// version: string; | |
// description?: string | undefined; | |
// metadata?: { | |
// [key: string]: any; | |
// } | undefined; | |
// settings: { | |
// [key: string]: string | number | boolean | undefined; | |
// }; | |
// } | |
console.log(config); | |
console.log(config.validate({ | |
name: "valid-example", | |
version: "1.0.0", | |
description: "An example configuration", | |
settings: { | |
theme: "dark", | |
notifications: true, | |
itemsPerPage: 20, | |
}, | |
})); // OK | |
console.log(config.validate({ | |
name: "error-prone-example", | |
version: "1.0.0", | |
// description: "An example configuration", | |
settings: { | |
theme: "dark", | |
notifications: true, | |
itemsPerPage: 20, | |
thisShouldCauseAnError: { objects: "are not allowed" }, | |
}, | |
unknownKey: "this should not be allowed", | |
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// deno-lint-ignore-file no-explicit-any | |
import { type Option, Some, None } from "./option.ts"; | |
/** | |
* This module provides the {@linkcode Result} type, which represents the | |
* result of an operation that can either succeed with a value of type `T` (`Ok`) | |
* or fail with an error of type `E` (`Err`). | |
* | |
* @see {@linkcode Ok} for the `Ok` variant. | |
* @see {@linkcode Err} for the `Err` variant. | |
* @see {@linkcode Result} for the public entrypoint of this API. | |
* | |
* @example | |
* ```ts | |
* import { Ok, Err } from "jsr:@type/result"; | |
* import { assert, assertEquals, assertThrows } from "jsr:@std/assert"; | |
* | |
* const a = Ok(10), b = Err("error"); | |
* | |
* assert(a.isOk()); // true | |
* assert(b.isErr()); // true | |
* | |
* assertEquals(a.unwrap(), 10); | |
* | |
* assertThrows(() => b.unwrap(), TypeError); | |
* // TypeError: Cannot call `Result.unwrap()` on an `Err` value: error | |
* ``` | |
* | |
* @category Result | |
* @module result | |
*/ | |
// #region Core | |
/** | |
* The `ResultType` class is the base class for the `Ok` and `Err` variants. | |
* | |
* It provides common methods and properties for both variants, such as | |
* `isOk`, `isErr`, `unwrap`, and `map`. Attempting to call methods like | |
* `unwrap` on an `Err` instance will throw a TypeError. | |
* | |
* @internal | |
*/ | |
export class ResultType<T, E> { | |
readonly #tag: "ok" | "err"; | |
readonly #value: T; // valid only if #tag === "ok" | |
readonly #error: E; // valid only if #tag === "err" | |
/** | |
* Constructs a new ResultType. | |
* | |
* @param tag - A discriminator: "ok" for success or "err" for failure. | |
* @param v - The contained value. If tag is "ok", this is of type T; | |
* if tag is "err", this is of type E. | |
*/ | |
constructor(tag: "ok" | "err", v: T | E) { | |
this.#tag = tag; | |
if (tag === "ok") { | |
this.#value = v as T; | |
// Bottom out error with a non-null assertion on undefined | |
this.#error = (undefined as never) as E; | |
} else { | |
// Bottom out value with a non-null assertion on undefined | |
this.#value = (undefined as never) as T; | |
this.#error = v as E; | |
} | |
} | |
/** | |
* Returns true if this is an `Ok` value, false otherwise. | |
* | |
* @example | |
* ```ts | |
* import { Ok, Err } from "jsr:@type/result"; | |
* | |
* const ok = Ok(42); | |
* const err = Err("oops"); | |
* console.log(ok.isOk()); // true | |
* console.log(err.isErr()); // true | |
* console.assert(!err.isOk() && !ok.isErr()); // OK | |
* ``` | |
*/ | |
isOk(): this is Ok<T> { | |
return this.#tag === "ok"; | |
} | |
/** | |
* Returns true if this is an `Err` value, false otherwise. | |
* | |
* @example | |
* ```ts | |
* import { Ok, Err } from "jsr:@type/result"; | |
* | |
* const ok = Ok(42); | |
* const err = Err("oops"); | |
* console.log(ok.isOk()); // true | |
* console.log(err.isErr()); // true | |
* console.assert(!err.isOk() && !ok.isErr()); // OK | |
* ``` | |
*/ | |
isErr(): this is Err<E> { | |
return this.#tag === "err"; | |
} | |
/** | |
* Returns the contained `Ok` value. | |
* If this is an `Err`, throws a TypeError. | |
* | |
* @example | |
* ```ts | |
* import { Ok, Err } from "jsr:@type/result"; | |
* | |
* const ok = Ok(42); | |
* console.log(ok.value); // 42 | |
* | |
* const err = Err("oops"); | |
* err.value; // throws TypeError | |
* ``` | |
*/ | |
get value(): T { | |
return this.unwrap(); | |
} | |
/** | |
* Returns the result of applying `andThen` to the contained `Ok` value. | |
* | |
* @example | |
* ```ts | |
* import { Ok } from "jsr:@type/result"; | |
* | |
* const res = Ok(2).andThen((x) => Ok(x * 3)); | |
* // res is Ok(6) | |
* ``` | |
*/ | |
andThen<U>(fn: (this: this, val: T) => Result<U, E>): Result<U, E> { | |
return this.isOk() | |
? fn.call(this, this.#value) | |
: new Err(this.#error); | |
} | |
/** | |
* | |
* @example | |
* ```ts | |
* import { Ok } from "jsr:@type/result"; | |
* | |
* const res = Ok(2).chain((x) => Ok(x * 3)); | |
* // res is Ok(6) | |
* ``` | |
*/ | |
chain<U>(fn: (this: this, val: T) => Result<U, E>): Result<U, E> { | |
return this.isOk() ? fn.call(this, this.#value) : new Err(this.#error); | |
} | |
/** | |
* If this is an `Err`, applies `fn` and returns the result; otherwise | |
* returns the contained `Ok` value. | |
* | |
* @example | |
* ```ts | |
* import { Err, Ok } from "jsr:@type/result"; | |
* | |
* const res = Err("error").chainErr((err) => Ok(err.length)); | |
* // if "error".length is 5, res is Ok(5) | |
* ``` | |
*/ | |
chainErr<F>(fn: (this: this, err: E) => Result<T, F>): Result<T, F> { | |
return this.isErr() | |
? fn.call(this, this.#error) | |
: new Ok(this.#value); | |
} | |
/** | |
* Converts this `Result` into an `Option` containing the `Err` value. | |
* If this is an `Ok`, returns `None`. | |
* | |
* @example | |
* ```ts | |
* import { Err, None } from "jsr:@type/result"; | |
* import { assertEquals } from "jsr:@std/assert"; | |
* | |
* assertEquals(Err("error").err(), Some("error")); | |
* assertEquals(Ok(10).err(), None); | |
* ``` | |
*/ | |
err(): Option<E> { | |
return this.isErr() ? Some(this.#error) : None; | |
} | |
/** | |
* If this is an `Ok`, returns a new `Err` wrapping `defaultErr`; | |
* otherwise returns this. | |
* | |
* @example | |
* ```ts | |
* import { Ok, Err } from "jsr:@type/result"; | |
* | |
* const res = Ok(10).errOr("default error"); | |
* // res is Err("default error") | |
* ``` | |
*/ | |
errOr(defaultErr: E): Result<T, E> { | |
return this.isOk() ? new Err(defaultErr) : this as never; | |
} | |
/** | |
* Returns a new result by applying `fn` to the contained `Ok` value. | |
* | |
* @example | |
* ```ts | |
* import { Ok } from "jsr:@type/result"; | |
* | |
* const r = Ok(2).flatMap((x) => Ok(x * 2)); | |
* // r is Ok(4) | |
* ``` | |
*/ | |
flatMap<U>(fn: (this: this, val: T) => Result<U, E>): Result<U, E> { | |
return this.isOk() | |
? fn.call(this, this.#value) | |
: new Err(this.#error); | |
} | |
/** | |
* If this is an `Ok` result, it is returned as-is. Otherwise, the provided | |
* `fn` is called with the contained `Err` value and a new result is returned | |
* containing the result of that function invocation. | |
* | |
* @param fn - A function that takes the contained `Err` value and returns a | |
* new `Result`. | |
* @returns A new `Result` containing the result of applying `fn` to the | |
* contained `Err` value. | |
* ```ts | |
* import { Err, Ok } from "jsr:@type/result"; | |
* | |
* const r = Err("fail").orElse((err) => Ok(String(err).length)); | |
* // if "fail".length is 4, r is Ok(4) | |
* ``` | |
*/ | |
orElse<F>(fn: (this: this, err: E) => Result<T, F>): Result<T, F> { | |
return this.isErr() | |
? fn.call(this, this.#error) | |
: new Ok(this.#value); | |
} | |
/** | |
* Returns a new result by applying `fn` to the contained `Err` value. | |
* | |
* @example | |
* ```ts | |
* import { Err } from "jsr:@type/result"; | |
* | |
* const r = Err("oops").flatMapErr((err) => Ok(String(err).length)); | |
* ``` | |
*/ | |
flatMapErr<F>(fn: (this: this, err: E) => Result<T, F>): Result<T, F> { | |
return this.isErr() | |
? fn.call(this, this.#error) | |
: new Ok(this.#value); | |
} | |
/** | |
* If this is an `Ok`, applies `fn` and returns the resulting `Result`. | |
* Otherwise returns the provided `defaultResult`. | |
* | |
* @example | |
* ```ts | |
* import { Ok, Err } from "jsr:@type/result"; | |
* | |
* const r1 = Ok(2).flatMapOr(Err("default"), (x) => Ok(x * 2)); | |
* // r1 is Ok(4) | |
* | |
* const r2 = Err("fail").flatMapOr(Ok(100), (x) => Ok(x * 2)); | |
* // r2 is Ok(100) | |
* ``` | |
*/ | |
flatMapOr<U>(defaultResult: Result<U, E>, fn: (this: this, val: T) => Result<U, E>): Result<U, E> { | |
return this.isOk() ? fn.call(this, this.#value) : defaultResult; | |
} | |
/** | |
* If this is an `Ok`, applies `fn` and returns the resulting `Result`. | |
* Otherwise computes an alternative via `defaultFn`. | |
* | |
* @example | |
* ```ts | |
* import { Err, Ok } from "jsr:@type/result"; | |
* | |
* const r = Err("fail").flatMapOrElse( | |
* (err) => Ok(String(err).length), | |
* (x) => Ok(x * 2), | |
* ); | |
* // if "fail".length is 4, r is Ok(4) | |
* ``` | |
*/ | |
flatMapOrElse<U>( | |
defaultFn: (this: this, err: E) => Result<U, E>, | |
fn: (this: this, val: T) => Result<U, E>, | |
): Result<U, E> { | |
return this.isOk() | |
? fn.call(this, this.#value) | |
: defaultFn.call(this, this.#error); | |
} | |
/** | |
* Calls `fn` with the contained `Ok` value and returns a new Result. | |
* | |
* @example | |
* ```ts | |
* import { Ok } from "jsr:@type/result"; | |
* | |
* const r = Ok(3).map((x) => x + 1); | |
* // r is Ok(4) | |
* ``` | |
*/ | |
map<U>(fn: (this: this, val: T) => U): Result<U, E> { | |
return this.isOk() | |
? new Ok(fn.call(this, this.#value)) | |
: new Err(this.#error); | |
} | |
/** | |
* Calls `fn` with the contained `Err` value and returns a new Result. | |
* | |
* @example | |
* ```ts | |
* import { Err } from "jsr:@type/result"; | |
* | |
* const r = Err("fail").mapErr((err) => err + "!"); | |
* // If err was "fail", returns Err("fail!") | |
* ``` | |
*/ | |
mapErr<F>(fn: (this: this, err: E) => F): Result<T, F> { | |
return this.isOk() | |
? new Ok(this.#value) | |
: new Err(fn.call(this, this.#error)); | |
} | |
/** | |
* If this is an `Err`, applies `fn` and returns the result; | |
* otherwise returns `defaultVal`. | |
* | |
* @example | |
* ```ts | |
* import { Err, Ok } from "jsr:@type/result"; | |
* | |
* const val = Err("fail").mapErrOr(100, (err) => String(err).length); | |
* // if "fail".length is 4, returns 4; if Ok, returns 100 | |
* ``` | |
*/ | |
mapErrOr<U>(defaultVal: U, fn: (this: this, err: E) => U): U { | |
return this.isErr() | |
? fn.call(this, this.#error) | |
: defaultVal; | |
} | |
/** | |
* If this is an `Err`, applies `fn` and returns the result; | |
* otherwise computes an alternative via `defaultFn`. | |
* | |
* @example | |
* ```ts | |
* import { Err, Ok } from "jsr:@type/result"; | |
* | |
* const val = Err("fail").mapErrOrElse( | |
* (ok) => 0, | |
* (err) => String(err).length | |
* ); | |
* // if "fail".length is 4, returns 4; if Ok, returns result of defaultFn | |
* ``` | |
*/ | |
mapErrOrElse<U>( | |
defaultFn: (this: this, val: T) => U, | |
fn: (this: this, err: E) => U, | |
): U { | |
return this.isErr() | |
? fn.call(this, this.#error) | |
: defaultFn.call(this, this.#value); | |
} | |
/** | |
* If this is an `Ok`, applies `fn` and returns the result; | |
* otherwise returns `defaultVal`. | |
* | |
* @example | |
* ```ts | |
* import { Ok, Err } from "jsr:@type/result"; | |
* | |
* Ok(5).mapOr(0, (x) => x * 2); // returns 10 | |
* Err("fail").mapOr(0, (x) => x * 2); // returns 0 | |
* ``` | |
*/ | |
mapOr<U>(defaultVal: U, fn: (this: this, val: T) => U): U { | |
return this.isOk() | |
? fn.call(this, this.#value) | |
: defaultVal; | |
} | |
/** | |
* If this is an `Ok`, applies `fn` and returns the result; | |
* otherwise computes an alternative via `defaultFn`. | |
* | |
* @example | |
* ```ts | |
* import { Ok, Err } from "jsr:@type/result"; | |
* | |
* Ok(5).mapOrElse( | |
* (err) => 0, | |
* (x) => x * 2 | |
* ); // returns 10 | |
* | |
* Err("fail").mapOrElse( | |
* (err) => String(err).length, | |
* (x) => x * 2 | |
* ); // returns "fail".length | |
* ``` | |
*/ | |
mapOrElse<U>( | |
defaultFn: (this: this, err: E) => U, | |
fn: (this: this, val: T) => U, | |
): U { | |
return this.isOk() | |
? fn.call(this, this.#value) | |
: defaultFn.call(this, this.#error); | |
} | |
/** | |
* Returns the contained `Ok` value if present; if this is an `Err`, throws a | |
* TypeError. | |
* | |
* @example | |
* ```ts | |
* import { Ok, Err } from "jsr:@type/result"; | |
* | |
* Ok(123).unwrap(); // returns 123 | |
* Err("bad").unwrap(); // throws TypeError: Cannot call `Result.unwrap()` on an `Err` value: bad | |
* ``` | |
*/ | |
unwrap(): T { | |
if (this.isOk()) { | |
return this.#value; | |
} | |
throw new TypeError("Cannot call `Result.unwrap()` on an `Err` value: " + String(this.#error)); | |
} | |
/** | |
* Returns the contained `Err` value if present; if this is an `Ok`, throws a | |
* TypeError. | |
* | |
* @example | |
* ```ts | |
* import { Ok, Err } from "jsr:@type/result"; | |
* | |
* Err("fail").unwrapErr(); // returns "fail" | |
* Ok(42).unwrapErr(); // throws TypeError: Cannot call `Result.unwrapErr()` on an `Ok` value: 42 | |
* ``` | |
*/ | |
unwrapErr(): E { | |
if (this.isErr()) return this.#error; | |
throw new TypeError("Cannot call `Result.unwrapErr()` on an `Ok` value: " + String(this.#value)); | |
} | |
/** | |
* Converts this `Result` into an `Option` of its `Ok` value. | |
* | |
* @example | |
* ```ts | |
* import { Ok, None } from "jsr:@type/result"; | |
* import { assertEquals } from "jsr:@std/assert"; | |
* | |
* assertEquals(Ok(10).toOption(), Some(10)); | |
* assertEquals(Err("error").toOption(), None); | |
* ``` | |
*/ | |
toOption(): Option<T> { | |
return this.ok(); | |
} | |
/** | |
* Converts this `Result` into an `Option` of its `Ok` value. | |
* | |
* @example | |
* ```ts | |
* import { Ok, None } from "jsr:@type/result"; | |
* import { assertEquals } from "jsr:@std/assert"; | |
* | |
* assertEquals(Ok(10).ok(), Some(10)); | |
* assertEquals(Err("error").ok(), None); | |
* ``` | |
*/ | |
ok(): Option<T> { | |
return this.isOk() ? Some(this.#value) : None; | |
} | |
/** | |
* If this is an `Ok`, returns this; otherwise returns a new `Ok` with | |
* `defaultVal`. | |
* | |
* @example | |
* ```ts | |
* import { Err, Ok } from "jsr:@type/result"; | |
* | |
* const res = Err("fail").okOr(100); | |
* // res is Ok(100) | |
* ``` | |
*/ | |
okOr(defaultVal: T): Result<T, E> { | |
return this.isOk() ? this : new Ok(defaultVal); | |
} | |
/** | |
* Returns a string representation of this Result for debugging purposes. | |
* | |
* @example | |
* ```ts | |
* import { Ok, Err } from "jsr:@type/result"; | |
* | |
* console.log(Ok(42).toString()); // "Ok(42)" | |
* console.log(Err("bad").toString()); // "Err(bad)" | |
* ``` | |
*/ | |
toString(): string { | |
return this.isOk() | |
? `Ok(${String(this.#value)})` | |
: `Err(${String(this.#error)})`; | |
} | |
*[Symbol.iterator](): IterableIterator<T | E> { | |
yield this.#value; | |
yield this.#error; | |
} | |
} | |
// #endregion Core | |
// #region Result | |
/** | |
* The `Result` type represents either a success (`Ok<T>`) or failure | |
* (`Err<E>`). | |
*/ | |
export type Result<T, E> = Ok<T> | Err<E>; | |
type ResultConstructor<T = unknown, E = unknown> = typeof ResultType<T, E>; | |
/** | |
* The `Result` constructor function creates a new `Result` instance from a | |
* given value and a flag indicating whether it is `Ok` or `Err`. | |
* | |
* @example | |
* ```ts | |
* import { Result } from "jsr:@type/result"; | |
* | |
* const ok = Result(10, true); | |
* const err = Result("fail", false); | |
* ``` | |
* | |
* @category Result | |
*/ | |
export interface ResultFactory extends ResultConstructor { | |
readonly prototype: Result<any, any>; | |
/** | |
* Creates a new {@linkcode Result} instance either as an `Ok` or `Err` based | |
* on the `isOk` parameter (defaults to `false`). | |
* | |
* **Note**: This is a low-level API and is primarily used internally. It is | |
* recommended that you always explicit create an `Ok` or `Err` instance via | |
* one of their own specific constructors, as it provides more clarity on the | |
* intent of the code and avoids potential confusion. | |
* | |
* @template T The type of the value contained in the `Ok` variant. | |
* @template E The type of the value contained in the `Err` variant. | |
*/ | |
new <T, E = never>(value: T, isOk: true): Result<T, E>; | |
<T = never, E = unknown>(error: E, isOk?: false): Result<T, E>; | |
new <T, E>(value: T | E, isOk?: boolean): Result<T, E>; | |
/** | |
* Creates a new {@linkcode Result} instance either as an `Ok` or `Err` based | |
* on the `isOk` parameter (defaults to `false`). | |
* | |
* **Note**: This is a low-level API and is primarily used internally. It is | |
* recommended that you always explicit create an `Ok` or `Err` instance via | |
* one of their own specific constructors, as it provides more clarity on the | |
* intent of the code and avoids potential confusion. | |
* | |
* @template T The type of the value contained in the `Ok` variant. | |
* @template E The type of the value contained in the `Err` variant. | |
* | |
* @param value The value to be wrapped in the `Result`. | |
* @param [isOk=false] The flag indicating whether this is `Ok` or `Err`. | |
* @returns a new {@linkcode Result} of the specified type. | |
*/ | |
<T, E = never>(value: T, isOk: true): Result<T, E>; | |
<T = never, E = unknown>(error: E, isOk?: false): Result<T, E>; | |
<T, E>(value: T | E, isOk?: boolean): Result<T, E>; | |
} | |
/** | |
* The `Result` function creates a new `Result` instance. | |
*/ | |
export const Result: ResultFactory = function (value, isOk = false) { | |
return isOk ? new Ok(value) : new Err(value); | |
} as ResultFactory; | |
// @ts-expect-error readonly property reassignment | |
Result.prototype = ResultType.prototype; | |
globalThis.Object.setPrototypeOf(Result, ResultType); | |
// #endregion Result | |
// #region Ok | |
/** | |
* The `Ok<T>` variant represents a successful result containing a value of type `T`. | |
*/ | |
export interface Ok<T> extends ResultType<T, never> {} | |
/** | |
* The {@linkcode Ok} constructor function creates a new `Ok` instance. | |
* | |
* @example | |
* ```ts | |
* import { Ok } from "jsr:@type/result"; | |
* | |
* const success = Ok(123); | |
* console.log(success.isOk()); // true | |
* console.log(success.unwrap()); // 123 | |
* ``` | |
* | |
* @category Result | |
*/ | |
export interface OkFactory extends ResultConstructor { | |
readonly prototype: Result<any, any>; | |
new <T>(value: T): Result<T, never>; | |
<T>(value: T): Result<T, never>; | |
} | |
export const Ok: OkFactory = function <T>(value: T): Result<T, never> { | |
return new ResultType<T, never>("ok", value); | |
} as OkFactory; | |
// @ts-expect-error readonly property reassignment | |
Ok.prototype = ResultType.prototype; | |
globalThis.Object.setPrototypeOf(Ok, Result); | |
// #endregion Ok | |
// #region Err | |
/** | |
* Represents a **failure** result, containing an error of type `E`. | |
*/ | |
export interface Err<E> extends ResultType<never, E> {} | |
/** | |
* Represents the call signatures of the {@linkcode Err} constructor/factory, | |
* which can be used to create a new `Err` instance from an error of type `E`. | |
* | |
* This function can be called as a standard function, or constructed with the | |
* `new` keyword like a class. | |
* | |
* @example | |
* ```ts | |
* import { Err } from "jsr:@type/result"; | |
* | |
* const failure = Err("something went wrong"); | |
* | |
* console.log(failure.isErr()); // true | |
* | |
* console.log(failure.unwrapErr()); // "something went wrong" | |
* | |
* const error2 = new Err("something else went wrong"); | |
* | |
* console.log(error2.mapErr((e) => `ERROR: ${e}!`).unwrapErr()); | |
* // "ERROR: something else went wrong!" | |
* ``` | |
* @category Result | |
*/ | |
export interface ErrFactory extends ResultConstructor { | |
readonly prototype: Result<any, any>; | |
new <E>(error: E): Result<never, E>; | |
<E>(error: E): Result<never, E>; | |
} | |
export const Err: ErrFactory = function <E>(error: E): Result<never, E> { | |
return new ResultType<never, E>("err", error) | |
} as ErrFactory; | |
// @ts-expect-error readonly property reassignment | |
Err.prototype = ResultType.prototype; | |
globalThis.Object.setPrototypeOf(Err, Result); | |
// #endregion Err |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// deno-lint-ignore-file no-explicit-any no-namespace | |
/// <reference lib="deno.unstable" /> | |
import { | |
isTemplateStringsArray, | |
} from "jsr:@type/is@~0.1/template-strings-array"; | |
import { inspect, type InspectOptions } from "node:util"; | |
// #region Validation Types | |
// #region fp-style stuff | |
const _tag: unique symbol = Symbol("tag"); | |
type _tag = typeof _tag; | |
interface CommonResult<T, E> { | |
// [_tag]: "Ok" | "Err"; | |
// readonly success: boolean; | |
// readonly data?: T; | |
// readonly error?: E; | |
unwrap(): T; | |
unwrapOr(fallback: T): T; | |
unwrapOrElse(fallback: () => T): T; | |
map<U>(fn: (data: T) => U): Result<U, E>; | |
mapOr<U>(fn: (data: T) => U, fallback: Result<U, E>): Result<U, E>; | |
mapOrElse<U>(fn: (data: T) => U, fallback: () => Result<U, E>): Result<U, E>; | |
flatMap<U>(fn: (data: T) => Result<U, E>): Result<U, E>; | |
flatMapOr<U>( | |
fn: (data: T) => Result<U, E>, | |
fallback: Result<U, E>, | |
): Result<U, E>; | |
flatMapOrElse<U>( | |
fn: (data: T) => Result<U, E>, | |
fallback: () => Result<U, E>, | |
): Result<U, E>; | |
and<U>(other: Result<U, E>): Result<U, E>; | |
andThen<U>(fn: (data: T) => Result<U, E>): Result<U, E>; | |
expect(message: string | ValidationError<E>): T; | |
expectErr(message: string | ValidationError<E>): E; | |
unwrapErr(): E; | |
unwrapErrOr(fallback: E): E; | |
unwrapErrOrElse(fallback: () => E): E; | |
mapErr<F>(fn: (error: E) => F): Result<T, F>; | |
mapErrOr<F>(fn: (error: E) => F, fallback: Result<T, F>): Result<T, F>; | |
mapErrOrElse<F>( | |
fn: (error: E) => F, | |
fallback: () => Result<T, F>, | |
): Result<T, F>; | |
flatMapErr<F>(fn: (error: E) => Result<T, F>): Result<T, F>; | |
flatMapErrOr<F>( | |
fn: (error: E) => Result<T, F>, | |
fallback: Result<T, F>, | |
): Result<T, F>; | |
flatMapErrOrElse<F>( | |
fn: (error: E) => Result<T, F>, | |
fallback: () => Result<T, F>, | |
): Result<T, F>; | |
match<U>(ok: (data: T) => U, err: (error: E) => U): U; | |
} | |
/** | |
* Represents a successful result of an operation, containing the resulting | |
* `data` value, if applicable, and a `success` flag set to `true`. | |
* | |
* @template [T=any] The type of the data value. | |
*/ | |
export interface Ok<T = any> extends CommonResult<T, never> { | |
readonly [_tag]: "Ok"; | |
readonly success: true; | |
readonly data: T; | |
} | |
/** | |
* Represents a failed result of an operation, containing an `error` message | |
* describing the failure, and a `success` flag set to `false`. | |
* | |
* @template [E=any] The type of the error value. | |
*/ | |
export interface Err<E = any> extends CommonResult<never, E> { | |
readonly [_tag]: "Err"; | |
readonly success: false; | |
readonly error: E; | |
} | |
export type Result<T, E = any> = Ok<T> | Err<E>; | |
/** | |
* Creates a new {@linkcode Ok} result object, indicating that the operation | |
* was successful and providing the resulting data value. | |
* | |
* @param data The data value to wrap in an {@linkcode Ok} result object. | |
* @returns A new {@linkcode Ok} result object containing the provided data. | |
*/ | |
// FIXME: why is this happening? over-recursion wasn't an issue before | |
// @ts-ignore -- type is too deep | |
export function Ok<T extends Type<any>>(data: T): Ok<Infer<T>>; | |
/** | |
* Creates a new {@linkcode Ok} result object, indicating that the operation | |
* was successful and providing the resulting data value. | |
* | |
* @param data The data value to wrap in an {@linkcode Ok} result object. | |
* @returns A new {@linkcode Ok} result object containing the provided data. | |
*/ | |
export function Ok<T = any>(data: T): Ok<T>; | |
/** @internal */ | |
export function Ok<T = any>(data: T): Ok<T> { | |
return { __proto__: Ok.prototype, success: true, data } as unknown as Ok<T>; | |
} | |
Ok.is = isOk; | |
Ok.unwrap = unwrap; | |
Ok.unwrapOr = unwrapOr; | |
Ok.unwrapOrElse = unwrapOrElse; | |
Ok.map = map; | |
Ok.mapOr = mapOr; | |
Ok.mapOrElse = mapOrElse; | |
Ok.expect = expect; | |
Ok.flatMap = flatMap; | |
Ok.flatMapOr = flatMapOr; | |
Ok.flatMapOrElse = flatMapOrElse; | |
Ok.flatMapErr = flatMapErr; | |
Ok.flatMapErrOr = flatMapErrOr; | |
Ok.flatMapErrOrElse = flatMapErrOrElse; | |
Ok.and = and; | |
Ok.andThen = andThen; | |
Ok.match = match; | |
Object.defineProperties(Ok, { | |
[Symbol.hasInstance]: { value: isOk, configurable: true }, | |
}); | |
export function Err<E>( | |
error: E, | |
options?: ValidationErrorOptions<E>, | |
): Err<E> { | |
if (typeof error === "string") { | |
return { | |
__proto__: Err.prototype, | |
success: false, | |
error: new ValidationError(error, options), | |
} as any; | |
} | |
return { __proto__: Err.prototype, success: false, error } as any; | |
} | |
Err.is = isErr; | |
Err.unwrap = unwrapErr; | |
Err.unwrapOr = unwrapErrOr; | |
Err.unwrapOrElse = unwrapErrOrElse; | |
Err.map = mapErr; | |
Err.mapOr = mapErrOr; | |
Err.mapOrElse = mapErrOrElse; | |
Err.expect = expectErr; | |
Err.flatMap = flatMapErr; | |
Err.flatMapOr = flatMapErrOr; | |
Err.flatMapOrElse = flatMapErrOrElse; | |
Err.and = and; | |
Err.andThen = andThen; | |
Object.defineProperties(Err, { | |
[Symbol.hasInstance]: { value: isErr, configurable: true }, | |
}); | |
const ResultPrototype = { | |
unwrap() { | |
return unwrap(this); | |
}, | |
unwrapOr(fallback) { | |
return unwrapOr(this, fallback); | |
}, | |
unwrapOrElse(fallback) { | |
return unwrapOrElse(this, fallback); | |
}, | |
map(fn) { | |
return map(this, fn); | |
}, | |
mapOr(fn, fallback) { | |
return mapOr(this, fn, fallback); | |
}, | |
mapOrElse(fn, fallback) { | |
return mapOrElse(this, fn, fallback); | |
}, | |
flatMap(fn) { | |
return flatMap(this, fn); | |
}, | |
flatMapOr(fn, fallback) { | |
return flatMapOr(this, fn, fallback); | |
}, | |
flatMapOrElse(fn, fallback) { | |
return flatMapOrElse(this, fn, fallback); | |
}, | |
expect(message) { | |
return expect(this, message); | |
}, | |
expectErr(message) { | |
return expectErr(this, message); | |
}, | |
unwrapErr() { | |
return unwrapErr(this); | |
}, | |
unwrapErrOr(fallback) { | |
return unwrapErrOr(this, fallback); | |
}, | |
unwrapErrOrElse(fallback) { | |
return unwrapErrOrElse(this, fallback); | |
}, | |
mapErr(fn) { | |
return mapErr(this, fn); | |
}, | |
mapErrOr(fn, fallback) { | |
return mapErrOr(this, fn, fallback); | |
}, | |
mapErrOrElse(fn, fallback) { | |
return mapErrOrElse(this, fn, fallback); | |
}, | |
flatMapErr(fn) { | |
return flatMapErr(this, fn); | |
}, | |
flatMapErrOr(fn, fallback) { | |
return flatMapErrOr(this, fn, fallback); | |
}, | |
flatMapErrOrElse(fn, fallback) { | |
return flatMapErrOrElse(this, fn, fallback); | |
}, | |
and(other) { | |
return and(this, other); | |
}, | |
andThen(fn) { | |
return andThen(this, fn); | |
}, | |
match(ok, err) { | |
return match(this, ok, err); | |
}, | |
} as CommonResult<any, any> & ThisType<Result<any, any>>; | |
Ok.prototype = ResultPrototype; | |
Err.prototype = ResultPrototype; | |
function isOk<T, E>(result: T | Result<T, E>): result is Ok<T>; | |
function isOk<T>(result: unknown): result is Ok<T>; | |
function isOk<T>(result: unknown): result is Ok<T> { | |
return result != null && typeof result === "object" && "success" in result && | |
"data" in result && result.success === true; | |
} | |
function isErr<T, E>(result: T | Result<T, E>): result is Err<E>; | |
function isErr<E>(result: unknown): result is Err<E>; | |
function isErr<E>(result: unknown): result is Err<E> { | |
return result != null && typeof result === "object" && "success" in result && | |
"error" in result && result.success === false; | |
} | |
function unwrap<T, E>(result: Result<T, E>): T { | |
if (!isOk(result)) throw new TypeError("Cannot unwrap an Err result"); | |
return result.data; | |
} | |
function unwrapOr<T, E>(result: Result<T, E>, fallback: T): T { | |
return isOk(result) ? result.data : fallback; | |
} | |
function unwrapOrElse<T, E>(result: Result<T, E>, fallback: () => T): T { | |
return isOk(result) ? result.data : fallback(); | |
} | |
function map<T, U, E>(result: Result<T, E>, fn: (data: T) => U): Result<U, E> { | |
return isOk(result) ? Ok(fn(result.data)) : result; | |
} | |
function mapOr<T, U, E>( | |
result: Result<T, E>, | |
fn: (data: T) => U, | |
fallback: Result<U, E>, | |
): Result<U, E> { | |
return isOk(result) ? Ok(fn(result.data)) : fallback; | |
} | |
function mapOrElse<T, U, E>( | |
result: Result<T, E>, | |
fn: (data: T) => U, | |
fallback: () => Result<U, E>, | |
): Result<U, E> { | |
return isOk(result) ? Ok(fn(result.data)) : fallback(); | |
} | |
function flatMap<T, U, E>( | |
result: Result<T, E>, | |
fn: (data: T) => Result<U, E>, | |
): Result<U, E> { | |
return isOk(result) ? fn(result.data) : result; | |
} | |
function flatMapOr<T, U, E>( | |
result: Result<T, E>, | |
fn: (data: T) => Result<U, E>, | |
fallback: Result<U, E>, | |
): Result<U, E> { | |
return isOk(result) ? fn(result.data) : fallback; | |
} | |
function flatMapOrElse<T, U, E>( | |
result: Result<T, E>, | |
fn: (data: T) => Result<U, E>, | |
fallback: () => Result<U, E>, | |
): Result<U, E> { | |
return isOk(result) ? fn(result.data) : fallback(); | |
} | |
function mapErr<T, E, F>( | |
result: Result<T, E>, | |
fn: (error: E) => F, | |
): Result<T, F> { | |
return isErr(result) ? Err(fn(result.error as E)) : result; | |
} | |
function mapErrOr<T, E, F>( | |
result: Result<T, E>, | |
fn: (error: E) => F, | |
fallback: Result<T, F>, | |
): Result<T, F> { | |
return isErr(result) ? Err(fn(result.error as E)) : fallback; | |
} | |
function mapErrOrElse<T, E, F>( | |
result: Result<T, E>, | |
fn: (error: E) => F, | |
fallback: () => Result<T, F>, | |
): Result<T, F> { | |
return isErr(result) ? Err(fn(result.error as E)) : fallback(); | |
} | |
function flatMapErr<T, E, F>( | |
result: Result<T, E>, | |
fn: (error: E) => Result<T, F>, | |
): Result<T, F> { | |
return isErr(result) ? fn(result.error) : result; | |
} | |
function flatMapErrOr<T, E, F>( | |
result: Result<T, E>, | |
fn: (error: E) => Result<T, F>, | |
fallback: Result<T, F>, | |
): Result<T, F> { | |
return isErr(result) ? fn(result.error) : fallback; | |
} | |
function flatMapErrOrElse<T, E, F>( | |
result: Result<T, E>, | |
fn: (error: E) => Result<T, F>, | |
fallback: () => Result<T, F>, | |
): Result<T, F> { | |
return isErr(result) ? fn(result.error) : fallback(); | |
} | |
function unwrapErr<T, E>(result: Result<T, E>): E { | |
if (isOk(result)) throw new TypeError("Cannot unwrap an Ok result"); | |
if (isErr(result)) return result.error; | |
throw new TypeError("Invalid Err object"); | |
} | |
function unwrapErrOr<T, E>(result: Result<T, E>, fallback: E): E { | |
return isErr(result) ? result.error : fallback; | |
} | |
function unwrapErrOrElse<T, E>(result: Result<T, E>, fallback: () => E): E { | |
return isErr(result) ? result.error : fallback(); | |
} | |
function expect<T, E>( | |
result: Result<T, E>, | |
message: string | ValidationError<E>, | |
): T { | |
if (isOk(result)) return result.data; | |
throw new TypeError(String(message)); | |
} | |
function expectErr<T, E>( | |
result: Result<T, E>, | |
message: string | ValidationError<E>, | |
): E { | |
if (isErr(result)) return result.error; | |
throw new TypeError(String(message)); | |
} | |
function and<T, U, E>( | |
result: Result<T, E>, | |
other: Result<U, E>, | |
): Result<U, E> { | |
return isOk(result) ? other : result; | |
} | |
function andThen<T, U, E>( | |
result: Result<T, E>, | |
fn: (data: T) => Result<U, E>, | |
): Result<U, E> { | |
return isOk(result) ? fn(result.data) : result; | |
} | |
function match<T, U, E>( | |
result: Result<T, E>, | |
ok: (data: T) => U, | |
err: (error: E) => U, | |
): U { | |
return isOk(result) ? ok(result.data) : err(result.error); | |
} | |
// #endregion fp-style stuff | |
type PathPart = string | number; | |
export type ValidationPath = PathPart | readonly PathPart[]; | |
export interface ValidationErrorOptions<T extends I = any, I = unknown> | |
extends ErrorOptions { | |
/** The path to the value that caused the error. */ | |
path?: ValidationPath; | |
/** The validator that threw the error. */ | |
validator?: Type<T, I>; | |
/** Human-readable representation of the Type's expected type. */ | |
expected?: string; | |
/** The value that caused the error. */ | |
value?: T; | |
} | |
export class ValidationError<T extends I = any, I = unknown> extends TypeError { | |
declare readonly [_type]: T; | |
declare readonly [_input]: I; | |
override readonly message: string; | |
readonly options: ValidationErrorOptions<T, I>; | |
constructor(message: string, options?: ValidationErrorOptions<T, I>); | |
constructor( | |
cause: ValidationError<T, I>, | |
options?: Omit<ValidationErrorOptions<T, I>, "cause">, | |
); | |
constructor( | |
error: string | ValidationError<T, any>, | |
options?: ValidationErrorOptions<T, any>, | |
); | |
constructor( | |
error: string | ValidationError, | |
options?: ValidationErrorOptions, | |
); | |
constructor( | |
message: string | ValidationError<T, I>, | |
options?: ValidationErrorOptions<T, I>, | |
) { | |
let cause: unknown; | |
if (typeof message !== "string") { | |
cause = message; | |
({ message = "Validation failed", options } = message ?? {}); | |
} else { | |
({ cause } = options ??= {} as ValidationErrorOptions<T, I>); | |
} | |
super(message, { cause }); | |
this.message = message; | |
this.options = options; | |
this.name = "ValidationError"; | |
if (cause instanceof Error) { | |
Error.captureStackTrace?.(this, cause.constructor); | |
} else { | |
Error.captureStackTrace?.(this, ValidationError); | |
} | |
this.stack; // force stack to be generated | |
Object.setPrototypeOf(this, ValidationError.prototype); | |
} | |
} | |
export class AggregateValidationError<T extends I = any, I = any> | |
extends ValidationError<T, I> { | |
constructor( | |
errors: Iterable<ValidationError<T, I>>, | |
message?: string, | |
options?: ValidationErrorOptions<T, I>, | |
) { | |
const cause = [...errors]; | |
message ??= `Multiple validation errors encountered (${cause.length}):\n`; | |
message += cause.map((e, i) => `${i + 1}. ${e.message}`).join("\n"); | |
super(message, { ...options, cause }); | |
} | |
get errors(): T { | |
return this.options?.cause as T; | |
} | |
} | |
// #endregion Validation Types | |
// #region Type Definitions | |
export type PrimitiveTypeMap = { | |
string: string; | |
number: number; | |
boolean: boolean; | |
bigint: bigint; | |
symbol: symbol; | |
undefined: undefined; | |
null: null; | |
}; | |
export type PrimitiveTypeName = string & keyof PrimitiveTypeMap; | |
export type NullishPrimitive = PrimitiveTypeMap[keyof PrimitiveTypeMap]; | |
export type TypeMap<A = never, B = never> = PrimitiveTypeMap & { | |
object: object; | |
function: ToFunctionType<A, B>; | |
}; | |
export type TypeName = string & keyof TypeMap; | |
// deno-lint-ignore ban-types | |
type strings = string & {}; | |
type ToFunctionType<A, B> = { | |
( | |
...args: [A] extends [never] ? any[] | |
: A extends readonly any[] ? A | |
: [B] extends [never] ? [] | |
: A[] | |
): [B] extends [never] ? [A] extends [never] ? any : A : B; | |
} extends infer S ? S : never; | |
// Object Types | |
type RequiredKeys<T> = keyof { | |
[K in keyof T as undefined extends Infer<T[K]> ? never : K]: K; | |
}; | |
type OptionalKeys<T> = keyof { | |
[K in keyof T as undefined extends Infer<T[K]> ? K : never]: K; | |
}; | |
type Reshape<T> = Pick<T, keyof T>; | |
type AbstractConstructor<T = any, A extends readonly any[] = any[]> = | |
abstract new (...args: A) => T; | |
// deno-fmt-ignore | |
type InnerInferSchemaType< | |
T extends Schema, | |
> = Reshape<{ | |
[K in RequiredKeys<T>]-?: T[K] extends AnyType ? Infer<T[K]> : T[K]; | |
} & { | |
[K in OptionalKeys<T>]+?: T[K] extends AnyType ? Infer<T[K]> : T[K]; | |
}>; | |
// deno-fmt-ignore | |
export type Infer<V> = | |
| [V] extends [never] ? never | |
: V extends { readonly [_type]: infer T } ? T | |
: V extends readonly Any[] | |
? [...{ [K in keyof V]: V[K] extends AnyType ? Infer<V[K]> : V[K] }] | |
: V extends Schema ? InnerInferSchemaType<V> | |
: V; | |
// deno-fmt-ignore | |
export type InputOf<T> = | |
| T extends { readonly [_type]: infer I } ? I | |
: T extends Type<any, infer I> ? I | |
: T; | |
// #endregion Type Definitions | |
// #region Constants and Metadata | |
export interface Metadata<T = any> { | |
name: string; | |
format?: string; | |
schema?: T; | |
message?: string; | |
subtypes?: Type<any>[]; | |
supertype?: Type<any>; | |
test?: ((data: any) => boolean) | ((data: any) => data is T); | |
coerce?: (data: any) => T; | |
convert?: (data: any) => T; | |
[key: string | symbol]: any; | |
} | |
export type ResolvedMetadata<T> = | |
& Required<Omit<Metadata<T>, "message" | "format">> | |
& Pick<Metadata<T>, "message" | "format">; | |
export interface StaticMetadata<T = any> { | |
glue: "&" | "|" | strings; | |
required?: boolean; | |
optional?: boolean; | |
nullable?: boolean; | |
readonly?: boolean; | |
message?: string; | |
prefix?: string; | |
suffix?: string; | |
format?: string; | |
schema?: T; | |
[key: string | symbol]: any; | |
} | |
/** | |
* Well-known symbol used to store internal metadata on an instance of the | |
* abstract {@linkcode Type} class. This is used to store information, | |
* configuration, and other data specific to a particular validator subclass, | |
* to allow for more accurate and informative error reporting. | |
* | |
* @category Symbols | |
* @internal | |
*/ | |
export const _metadata: unique symbol = Symbol("Type.#metadata"); | |
export type _metadata = typeof _metadata; | |
export const _type: unique symbol = Symbol("Type.#type"); | |
export type _type = typeof _type; | |
export const _input: unique symbol = Symbol("Type.#input"); | |
export type _input = typeof _input; | |
// #endregion Constants and Metadata | |
// #region Type (Abstract Base Class) | |
export interface Type<T extends I = any, I = unknown> { | |
readonly [_input]: I; | |
readonly [_type]: T; | |
(input: I): T; | |
// is(data: unknown): data is T; | |
// assert(data: unknown): asserts data is T; | |
// coerce(data: unknown): T; | |
// convert(data: unknown): T; | |
} | |
export abstract class Type<T extends I = any, I = unknown> extends Function { | |
/** | |
* The name of the validator, used for error messages and debugging. | |
* | |
* @remarks | |
* This should be a human-readable string that describes the type of data | |
* that the validator expects, such as `"string"` or `"Partial<User>"`. | |
*/ | |
override readonly name: string; | |
#metadata: Metadata<T>; | |
constructor(meta: Metadata<T>); | |
constructor(name: string, meta?: Partial<Metadata<T>>); | |
constructor( | |
name: string, | |
test: (it: I) => it is T, | |
convert?: (input: I) => T, | |
); | |
constructor( | |
name: string | Metadata<T>, | |
test?: ((it: I) => it is T) | Partial<Metadata<T>>, | |
convert?: (input: I) => T, | |
) { | |
super("...args", "return this.convert(...args)"); | |
let meta = { name: "unknown" } as Metadata<T>; | |
if (typeof name === "string") { | |
meta = { ...meta, name }; | |
} else if (name != null && typeof name === "object") { | |
meta = { ...meta, ...name }; | |
} | |
if (typeof test === "function") { | |
meta = { ...meta, test }; | |
} else if (test != null && typeof test === "object") { | |
meta = { ...meta, ...test }; | |
} | |
if (typeof convert === "function") meta = { ...meta, convert }; | |
if (!meta.name || typeof meta.name !== "string") { | |
throw new TypeError("Validator name must be a string"); | |
} | |
this.name = meta.name; | |
this.#metadata = { | |
coerce: (data) => { | |
if (meta.coerce) return meta.coerce(data); | |
this.assert(data); | |
return data; | |
}, | |
convert: (data) => { | |
if (meta.convert) return meta.convert(data); | |
if (meta.coerce) return meta.coerce(data); | |
this.assert(data); | |
return data; | |
}, | |
test: (data) => this.validate(data).success, | |
...meta, | |
}; | |
const cache = new WeakMap(); | |
const self = this as any; | |
return new Proxy(this, { | |
apply: (target, _, args) => { | |
return target.convert.call(this, ...args as [I]); | |
}, | |
get: (_t, p) => { | |
let v = this[p as keyof this] as any; | |
if (v === undefined && this instanceof SchemaType) { | |
// allow direct access to schema properties | |
const schema = self[_metadata].schema; | |
if (schema && p in schema) { | |
v = schema[p as keyof typeof schema]; | |
} | |
} | |
if (typeof v === "function") { | |
if (cache.has(v)) return cache.get(v); | |
const bound = v.bind(self); | |
Object.defineProperty(bound, "name", { value: v.name }); | |
cache.set(v, bound); | |
return bound; | |
} else { | |
return v; | |
} | |
}, | |
}); | |
} | |
/** | |
* Validates the provided input `data` against this type's schema, returning | |
* a {@linkcode Result} object that indicates whether or not the value meets | |
* the constraints of the type. | |
* | |
* If `data` is valid, an {@linkcode Ok} result will be returned, containing | |
* the coerced/converted value and a `success` flag with a value of `true`. | |
* | |
* Otherwise, an {@linkcode Err} result will be returned, containing a | |
* `success` flag of `false` and an `error` property with one or more error | |
* messages describing the validation failure(s). | |
* | |
* **Note**: every type is required to implement this method, as it is the | |
* primary means of validating data against the type's schema. It is also | |
* used internally by the default implementations for the other derivative | |
* methods of a type, including {@linkcode is}, {@linkcode assert}, etc. | |
* | |
* @param data The input data to validate against the type's schema. | |
* @returns A {@linkcode Result} object indicating the success or failure of | |
* the validation, along with any coerced or converted data (if applicable). | |
* @example | |
* ```ts | |
* import * as t from "jsr:@type/kit@~0.1"; | |
* | |
* // repository for reusable type definitions | |
* const r = t.repository({ | |
* role: t.enum("admin", "user", "guest"), | |
* status: t.enum("active", "inactive", "pending"), | |
* // other types, which are now accessible as `r.role`, `r.status`, etc. | |
* }); | |
* | |
* // user type schema definition | |
* const user = t.object({ | |
* name: t.string, | |
* age: t.integer, | |
* email: t.optional(t.string), | |
* address: t.optional({ | |
* street: t.string.min(5), | |
* city: t.string.min(3), | |
* zip: t.integer.min(10000).max(99999), | |
* state: t.string.length(2), | |
* }), | |
* roles: r.role.array.min(1), | |
* }); | |
* ``` | |
*/ | |
abstract validate(data: unknown): Result<T>; | |
protected get [_metadata](): ResolvedMetadata<T> { | |
return this.#metadata as ResolvedMetadata<T>; | |
} | |
is(value: unknown): value is T { | |
return this.validate(value).success; | |
} | |
assert(value: unknown): asserts value is T { | |
const result = this.validate(value); | |
if (!result.success) { | |
const error = new ValidationError(result.error, { | |
validator: this, | |
value, | |
}); | |
Error.captureStackTrace?.(error, this.assert); | |
error.stack; // force stack to be generated | |
throw error; | |
} | |
} | |
coerce(value: unknown): T { | |
return this[_metadata].coerce.call(this, value); | |
} | |
convert(value: I): T { | |
return this[_metadata].convert.call(this, value); | |
} | |
inspect(options?: InspectOptions): string { | |
const { depth: _depth = 2 } = options ?? {}; | |
return this.toString(); | |
} | |
override toString(): string { | |
return this[_metadata].name; | |
} | |
[inspect.custom](depth: number | null, options: InspectOptions): string { | |
depth = typeof depth === "number" ? depth : options?.depth ?? 2; | |
options = (typeof depth === "number" ? options : depth ?? options) ?? {}; | |
const { name } = this[_metadata]; | |
const s = (v: any, t: string) => (options as any).stylize?.(v, t) ?? v; | |
if (depth <= 0) return s(`[${name}]`, "special"); | |
return `${name} ${inspect({ ...this }, { ...options, depth })}`; | |
} | |
static of(data: unknown): string { | |
let type = data === null ? "null" : typeof data; | |
if (type === "object") { | |
const tag = Object.prototype.toString.call(data).slice(8, -1); | |
if (tag !== "Object") type = tag; | |
return type === "Object" ? "object" : type; | |
} | |
return type; | |
} | |
static #_metadata: StaticMetadata<typeof Any> = { | |
glue: "|", | |
}; | |
static get [_metadata](): StaticMetadata<typeof Any> { | |
return this.#_metadata; | |
} | |
static render<T extends Any>(this: typeof Any, ...types: T[]): string; | |
static render<T extends Any>(this: typeof Any, types: T[]): string; | |
static render(...types: unknown[]): string; | |
static render(...types: any[]): string { | |
const glue = this[_metadata].glue ?? "|"; | |
return types.flat().filter((t) => !!t).map((t, _i, a) => { | |
if (a.length > 1) { | |
const wrap = (t: any, n: number) => n > 1 ? `(${t})` : `${t}`; | |
if (t instanceof Union) return wrap(t, (t as any).length); | |
if (t instanceof Intersection) return wrap(t, (t as any).length); | |
if (t instanceof Optional) return `${t}?`; | |
if (t instanceof Literal) { | |
return inspect(t.value, { colors: !Deno.noColor }); | |
} | |
} | |
return String(t); | |
}).join(` ${glue} `); | |
} | |
} | |
export declare namespace Type { | |
export { _metadata as metadata, _type as type }; | |
} | |
export namespace Type { | |
Type.metadata = _metadata; | |
Type.type = _type; | |
} | |
export type AnyType = Type<any, any>; | |
export type UnknownType = Type<unknown, unknown>; | |
export type NeverType = Type<never, unknown>; | |
// #endregion Type (Abstract Base Class) | |
// #region helpers | |
// deno-lint-ignore ban-types | |
type unknowns = {} | null | undefined; | |
const _fmt_options: unique symbol = Symbol("fmt.options"); | |
/** | |
* Formats a template string, interpolating values by inspecting them using | |
* the `util.inspect` function. Inspect options can be provided using the | |
* `fmt.options` utility: it can be called as a function, in which case it | |
* expects to receive an options object for its only argument, and returns a | |
* new `fmt` function with the given options applied. Alternatively, it can | |
* also be applied as an interpolated value in an outer `fmt` template string, | |
* in which case its options will be applied to the outer template string. | |
* | |
* @category String Utilities | |
* | |
* @example | |
* ```ts | |
* const options = fmt.options({ colors: true }); | |
* | |
* const message1 = fmt`Hello, ${options}world!`; | |
* console.log(message1); // => \u001b[32m"Hello, world!"\u001b[39m | |
* | |
* const message2 = fmt.options({ colors: true })`Hello, world!`; | |
* console.log(message2); // => \u001b[32m"Hello, world!"\u001b[39m | |
* | |
* const message3 = fmt({ colors: true })`Hello, world!`; | |
* console.log(message3); // => \u001b[32m"Hello, world!"\u001b[39m | |
* ``` | |
*/ | |
export function fmt( | |
strings: TemplateStringsArray, | |
...values: ReadonlyArray<unknowns | typeof fmt> | |
): string; | |
export function fmt(options: InspectOptions): typeof fmt; | |
export function fmt( | |
stringsOrOptions: TemplateStringsArray | InspectOptions, | |
...values: unknown[] | |
): string | typeof fmt { | |
function format( | |
strings: TemplateStringsArray, | |
values: any[], | |
options: InspectOptions, | |
): string { | |
let currentOptions = options; | |
const result = strings.reduce((acc, str, index) => { | |
let value = values[index - 1]; | |
// Inline options handling | |
if (typeof value === "function" && value[_fmt_options]) { | |
currentOptions = { ...currentOptions, ...value[_fmt_options] }; | |
value = ""; // Replace inline options with an empty string | |
} else if ((index - 1) in values) { | |
value = inspect(value, currentOptions); | |
} | |
return acc + value + str; | |
}); | |
return result; | |
} | |
function withOptions(options: InspectOptions) { | |
const customFmt = ( | |
strings: TemplateStringsArray, | |
...values: unknown[] | |
): string => format(strings, values, options); | |
customFmt[_fmt_options] = options; | |
return customFmt; | |
} | |
// Distinguish between direct template call and options configuration | |
if (isTemplateStringsArray(stringsOrOptions)) { | |
// Handle template string directly | |
return format(stringsOrOptions, values, { | |
colors: true, | |
depth: 2, | |
getters: "get", | |
maxArrayLength: 25, | |
maxStringLength: 50, | |
breakLength: 80, | |
compact: true, | |
}); | |
} else { | |
// Return a pre-configured fmt function if options are provided | |
return withOptions(stringsOrOptions as InspectOptions) as any; | |
} | |
} | |
// #endregion helpers | |
// #region Abstracts | |
export class Any extends Type<any, any> { | |
constructor() { | |
super("any"); | |
Object.setPrototypeOf(this, Any.prototype); | |
} | |
validate(data: any): Result<any> { | |
return Ok(data); | |
} | |
} | |
export class Unknown extends Type<unknown, unknown> { | |
constructor() { | |
super("unknown"); | |
Object.setPrototypeOf(this, Unknown.prototype); | |
} | |
validate(data: unknown): Result<unknown> { | |
return Ok(data); | |
} | |
} | |
export class Never extends Type<never, unknown> { | |
constructor() { | |
super("never"); | |
Object.setPrototypeOf(this, Never.prototype); | |
} | |
validate(_: unknown): Result<never> { | |
return Err("Never type cannot be instantiated"); | |
} | |
} | |
// #endregion Abstracts | |
// #region Literal | |
export class Literal<const T> extends Type<T> { | |
readonly #value: T; | |
constructor( | |
value: T, | |
meta?: Partial<Metadata<T>>, | |
) { | |
super(`Literal<${inspect(value)}>`, meta); | |
this.#value = value; | |
Object.setPrototypeOf(this, Literal.prototype); | |
} | |
get value(): T { | |
return this.#value; | |
} | |
validate(data: unknown): Result<T> { | |
if (sameValueNonZero(this.value, data)) { | |
return Ok(this.value); | |
} else { | |
return Err(fmt`Expected literal ${this}, but received ${data}`); | |
} | |
} | |
override inspect(options?: InspectOptions | null | undefined): string { | |
return inspect(this.value, { colors: !Deno.noColor, ...options }); | |
} | |
override valueOf(): T { | |
return this.value; | |
} | |
} | |
function sameValueNonZero(a: any, b: any): boolean { | |
if (a === b) { | |
if (typeof a !== "number") return true; | |
return (a !== 0 && b !== 0) || (1 / a === 1 / b); | |
} | |
return a !== a && b !== b; // NaN | |
} | |
export class Null extends Literal<null> { | |
constructor() { | |
super(null, { coerce: () => null }); | |
Object.setPrototypeOf(this, Null.prototype); | |
} | |
} | |
export class Void extends Literal<void> { | |
constructor() { | |
super(void 0 as void, { coerce: () => {} }); | |
Object.setPrototypeOf(this, Void.prototype); | |
} | |
} | |
// #endregion Literal | |
// #region Primitive | |
export class Primitive<T extends NullishPrimitive> extends Type<T> { | |
#type: PrimitiveTypeName; | |
constructor( | |
type: PrimitiveTypeName, | |
meta?: Partial<Metadata<T>>, | |
) { | |
super(type, meta); | |
this.#type = type; | |
Object.setPrototypeOf(this, Primitive.prototype); | |
} | |
validate(value: unknown): Result<T> { | |
const type = Type.of(value); | |
if (type === this.#type) { | |
const data = value as T; | |
return Ok(data); | |
} else { | |
return Err( | |
fmt`Expected a primitive ${this.#type} value, but received ${value} (${type})`, | |
); | |
} | |
} | |
override toString(): string { | |
return this.#type; | |
} | |
} | |
export class StringType extends Primitive<string> { | |
constructor() { | |
super("string", { coerce: String }); | |
Object.setPrototypeOf(this, StringType.prototype); | |
} | |
min(): number | undefined; | |
min(length: number): this; | |
min(length?: number): this | number | undefined { | |
if (typeof length === "undefined") { | |
return this[_metadata].min; | |
} else if (!isNaN(length = +length)) { | |
this[_metadata].min = length >>> 0; | |
} else { | |
throw new TypeError("Expected a non-negative integer value"); | |
} | |
return this; | |
} | |
max(): number | undefined; | |
max(length: number): this; | |
max(length?: number): this | number | undefined { | |
if (typeof length === "undefined") { | |
return this[_metadata].max; | |
} else if (!isNaN(length = +length)) { | |
this[_metadata].max = length >>> 0; | |
} else { | |
throw new TypeError("Expected a non-negative integer value"); | |
} | |
return this; | |
} | |
len(): number | undefined; | |
len(length: number): this; | |
len(length?: number): this | number | undefined { | |
if (typeof length === "undefined") { | |
return this[_metadata].length; | |
} else if (!isNaN(length = +length)) { | |
this[_metadata].length = length >>> 0; | |
} else { | |
throw new TypeError("Expected a non-negative integer value"); | |
} | |
return this; | |
} | |
pattern(): RegExp | undefined; | |
pattern(regex: RegExp): this; | |
pattern(regex?: RegExp): this | RegExp | undefined { | |
if (typeof regex === "undefined") { | |
return this[_metadata].pattern; | |
} else if (regex instanceof RegExp) { | |
this[_metadata].pattern = regex; | |
} else { | |
throw new TypeError("Expected a regular expression pattern"); | |
} | |
return this; | |
} | |
override validate(data: unknown): Result<string> { | |
const result = super.validate(data); | |
if (!result.success) return result; | |
const { min, max, length, pattern } = this[_metadata]; | |
const str = result.data as string; | |
const len = str.length; | |
if (typeof length === "number" && len !== length) { | |
return Err( | |
fmt`Expected string of length ${length}, but received ${len}: ${str}`, | |
); | |
} | |
if (typeof min === "number" && len < min) { | |
return Err( | |
fmt`Expected string with at least ${min} characters, but received ${len}: ${str}`, | |
); | |
} | |
if (typeof max === "number" && len > max) { | |
return Err( | |
fmt`Expected string with at most ${max} characters, but received ${len}: ${str}`, | |
); | |
} | |
if (pattern instanceof RegExp && !pattern.test(str)) { | |
return Err( | |
fmt`Expected string to match pattern /${pattern.source}/, but received: ${str}`, | |
); | |
} | |
return Ok(str); | |
} | |
} | |
export class NumberType extends Primitive<number> { | |
constructor(min?: number | undefined, max?: number | undefined) { | |
super("number", { coerce: Number }); | |
Object.setPrototypeOf(this, NumberType.prototype); | |
if (typeof min === "number") this.min(min); | |
if (typeof max === "number") this.max(max); | |
} | |
min(): number | undefined; | |
min(value: number): this; | |
min(value?: number): this | number | undefined { | |
if (typeof value === "undefined") { | |
return this[_metadata].min; | |
} else if (!isNaN(value = +value)) { | |
this[_metadata].min = value; | |
} else { | |
throw new TypeError("Expected a numeric value"); | |
} | |
return this; | |
} | |
max(): number | undefined; | |
max(value: number): this; | |
max(value?: number): this | number | undefined { | |
if (typeof value === "undefined") { | |
return this[_metadata].max; | |
} else if (!isNaN(value = +value)) { | |
this[_metadata].max = value; | |
} else { | |
throw new TypeError("Expected a numeric value"); | |
} | |
return this; | |
} | |
integer(value?: boolean | undefined): this { | |
this[_metadata].integer = !!(value ?? true); | |
return this; | |
} | |
override validate(data: unknown): Result<number> { | |
const result = super.validate(data); | |
if (!result.success) return result; | |
const { min, max, integer } = this[_metadata]; | |
const num = result.data as number; | |
if (typeof min === "number" && num < min) { | |
return Err( | |
fmt`Expected number to be at least ${min}, but received ${num}`, | |
); | |
} | |
if (typeof max === "number" && num > max) { | |
return Err( | |
fmt`Expected number to be at most ${max}, but received ${num}`, | |
); | |
} | |
if (integer && num % 1 !== 0) { | |
return Err(fmt`Expected an integer, but received ${num}`); | |
} | |
return Ok(num); | |
} | |
} | |
export class BigIntType extends Primitive<bigint> { | |
constructor(min?: bigint | undefined, max?: bigint | undefined) { | |
super("bigint", { coerce: BigInt }); | |
Object.setPrototypeOf(this, BigIntType.prototype); | |
if (min) this.min(min); | |
if (max) this.max(max); | |
} | |
min(): bigint | undefined; | |
min(value: bigint): this; | |
min(value?: bigint): this | bigint | undefined { | |
if (typeof value === "undefined") { | |
return this[_metadata].min; | |
} else if (typeof value === "bigint") { | |
this[_metadata].min = value; | |
} else { | |
throw new TypeError("Expected a BigInt value"); | |
} | |
return this; | |
} | |
max(): bigint | undefined; | |
max(value: bigint): this; | |
max(value?: bigint): this | bigint | undefined { | |
if (typeof value === "undefined") { | |
return this[_metadata].max; | |
} else if (typeof value === "bigint") { | |
this[_metadata].max = value; | |
} else { | |
throw new TypeError("Expected a BigInt value"); | |
} | |
return this; | |
} | |
override validate(data: unknown): Result<bigint> { | |
const result = super.validate(data); | |
if (!result.success) return result; | |
const { min, max } = this[_metadata]; | |
const num = result.data as bigint; | |
if (typeof min === "bigint" && num < min) { | |
return Err( | |
fmt`Expected bigint to be at least ${min}, but received ${num}`, | |
); | |
} | |
if (typeof max === "bigint" && num > max) { | |
return Err( | |
fmt`Expected bigint to be at most ${max}, but received ${num}`, | |
); | |
} | |
return Ok(num); | |
} | |
} | |
export class BooleanType extends Primitive<boolean> { | |
constructor() { | |
super("boolean", { coerce: Boolean }); | |
Object.setPrototypeOf(this, BooleanType.prototype); | |
} | |
} | |
export class SymbolType extends Primitive<symbol> { | |
constructor() { | |
super("symbol", { coerce: Symbol }); | |
Object.setPrototypeOf(this, SymbolType.prototype); | |
} | |
} | |
const _wellknown: unique symbol = Symbol("@@symbol::well-known"); | |
type _wellknown = typeof _wellknown; | |
type WellKnownSymbolKeys = keyof { | |
[K in keyof typeof Symbol as typeof Symbol[K] extends symbol ? K : never]: K; | |
}; | |
type WellKnownSymbolFor<K extends WellKnownSymbolKeys> = typeof Symbol[K]; | |
export class WellKnownSymbol< | |
K extends WellKnownSymbolKeys, | |
> extends Literal<WellKnownSymbolFor<K>> { | |
declare readonly [_wellknown]: true; | |
constructor(readonly key: K) { | |
super(Symbol[key], { coerce: () => Symbol[key] }); | |
Object.setPrototypeOf(this, WellKnownSymbol.prototype); | |
} | |
} | |
export class Undefined extends Primitive<undefined> { | |
constructor() { | |
super("undefined", { coerce: () => undefined }); | |
Object.setPrototypeOf(this, Undefined.prototype); | |
} | |
} | |
export class ObjectType extends Type<object> { | |
constructor() { | |
super("object", { coerce: Object }); | |
Object.setPrototypeOf(this, ObjectType.prototype); | |
} | |
validate(data: unknown): Result<object> { | |
if (typeof data === "object" && data !== null) { | |
return Ok(data); | |
} else { | |
return Err(fmt`Expected an object, but received ${data}`); | |
} | |
} | |
} | |
// #endregion Primitive | |
// #region Object | |
// #region SchemaType (internal) | |
const _shape: unique symbol = Symbol("SchemaType.#shape"); | |
type _shape = typeof _shape; | |
type Schema<K extends PropertyKey = PropertyKey> = { | |
[key in K]: Any | undefined; | |
}; | |
export class SchemaType< | |
const T extends Schema = Schema, | |
> extends Type<Infer<T>> { | |
static readonly #registry = new WeakMap<SchemaType, Schema>(); | |
static readonly #id_cache = new WeakMap<Schema, number>(); | |
constructor(shape: T | SchemaType<T>) { | |
const schema = shape instanceof SchemaType ? shape[_shape] : shape; | |
const name = SchemaType.render(schema, "Schema"); | |
super(name, { schema: schema as unknown as Infer<T> }); | |
// for (const key in shape) { | |
// if (!Object.hasOwn(shape, key)) continue; | |
// if (key === "constructor" || key === "prototype" || key === "name") { | |
// continue; | |
// } | |
// const value = shape[key]; | |
// (this as any)[key] = value; | |
// } | |
Object.setPrototypeOf(this, SchemaType.prototype); | |
SchemaType.#registry.set(this, schema); | |
} | |
get [_shape](): T { | |
return this[_metadata].schema as T; | |
} | |
validate(data: unknown): Result<Infer<T>> { | |
if (typeof data !== "object" || data === null || Array.isArray(data)) { | |
return Err( | |
fmt`Expected an object, but received ${Type.of(data)}: ${data}`, | |
); | |
} | |
const result = {} as Record<PropertyKey, any>; | |
const errors = []; | |
for (const key in this[_shape]) { | |
const guard = this[_shape][key]; | |
const value = (data as T)[key]; | |
if (typeof value === "undefined") { | |
const undef = guard?.validate(undefined); | |
if (undef && !undef?.success) { | |
errors.push( | |
new ValidationError(fmt`Missing required property ${key}`, { | |
path: [key], | |
cause: undef?.error, | |
validator: guard, | |
expected: guard?.toString(), | |
}), | |
); | |
} else if (undef && typeof undef?.data !== "undefined") { | |
result[key] = undef.data; | |
} | |
} else { | |
const res = guard?.validate(value); | |
if (res && !res?.success) { | |
errors.push( | |
new ValidationError<any>( | |
fmt`Invalid value for key ${key}: ${res?.error}`, | |
{ | |
path: [key], | |
cause: res?.error, | |
value, | |
expected: guard?.toString(), | |
}, | |
), | |
); | |
} else if (res?.data) { | |
result[key] = res.data; | |
} | |
} | |
} | |
if (errors.length) return Err(new AggregateValidationError(errors)); | |
return Ok<any>(result); | |
} | |
static override render<const T extends Schema>( | |
schema: T, | |
tag = "", | |
{ | |
colors = !getNoColor(), | |
compact = false as number | boolean, | |
maxArrayLength = 25, | |
maxStringLength = 50, | |
depth = 10 as number | null, | |
breakLength = 80, | |
numericSeparator = false, | |
getters = "get" as "get" | "set" | boolean, | |
sorted = false, | |
showHidden = false, | |
customInspect = false, | |
}: InspectOptions = {}, | |
): string { | |
const o = { | |
colors, | |
numericSeparator, | |
breakLength, | |
maxArrayLength, | |
maxStringLength, | |
depth, | |
getters, | |
compact, | |
sorted, | |
showHidden, | |
customInspect, | |
}; | |
const structure = Object.entries(schema).reduce( | |
(p, [k, v], i, a) => | |
!v | |
? "" | |
: `${p || "{"}${compact ? " " : "\n "}${k}${ | |
v instanceof Optional ? "?" : "" | |
}: ${v.inspect(o)}${i < a.length - 1 ? "," : ""}${ | |
compact ? "" : "\n" | |
}`, | |
"", | |
); | |
return tag ? `${tag}<${structure}>` : structure; | |
} | |
static hash<const T extends Schema>( | |
schema: T, | |
): number { | |
let id = this.#id_cache.get(schema); | |
if (!id) { | |
id = ( | |
Object.keys(schema) as (string & keyof T)[] | |
).reduce((h, k, i) => { | |
const t = schema[k]; | |
for (let j = 0; j < k.length; j++) { | |
h = (h << 5) - h + k.charCodeAt(j); | |
h >>>= 0; | |
} | |
h = (h << 5) - h; | |
h += t?.[_metadata]?.name?.length ?? i; | |
h >>>= 0; | |
return h; | |
}, 0); | |
this.#id_cache.set(schema, id); | |
} | |
return id; | |
} | |
} | |
// type SchemaTypeConstructor = { | |
// readonly prototype: ISchemaType<any>; | |
// new <const T extends Schema>(shape: T): SchemaType<T>; | |
// } & typeof SchemaTypeConstructor; | |
// type ISchemaType<T extends Schema> = InstanceType< | |
// typeof SchemaTypeConstructor<T> | |
// >; | |
// type InnerSchemaType< | |
// T extends Schema, | |
// Root extends boolean = false, | |
// > = Root extends true ? InnerSchemaType<T> & { [K in keyof T]: Infer<T[K]> } | |
// : ISchemaType<T>; | |
// #endregion SchemaType (internal) | |
// export const SchemaType: SchemaTypeConstructor = | |
// SchemaTypeConstructor as SchemaTypeConstructor; | |
// export type SchemaType<T extends Schema> = InnerSchemaType< | |
// Infer<T>, | |
// true | |
// >; | |
export function getNoColor(): boolean { | |
if (typeof globalThis.Deno === "object" && globalThis.Deno !== null) { | |
return globalThis.Deno.noColor; | |
} else if ( | |
typeof globalThis.process === "object" && globalThis.process !== null | |
) { | |
const e = globalThis.process.env ?? {}; | |
return e.NO_COLOR === "1" || e.NO_COLOR === "true" || e.CLICOLOR === "0" || | |
e.CLICOLOR === "false" || e.FORCE_COLOR === "0" || | |
e.NODE_DISABLE_COLORS === "1" || e.TERM === "dumb" || | |
!globalThis.process.stdout.isTTY; | |
} else { | |
return true; | |
} | |
} | |
// #endregion Object | |
// #region Record | |
export class RecordType<K extends PropKey, V extends Any = Any> | |
extends Type<Record<Infer<K>, Infer<V>>> { | |
constructor(protected keyType: K, protected valueType: V) { | |
super(`Record<${keyType}, ${valueType}>`); | |
Object.setPrototypeOf(this, RecordType.prototype); | |
} | |
validate(data: unknown): Result<Record<Infer<K>, Infer<V>>> { | |
if (typeof data !== "object" || data === null || Array.isArray(data)) { | |
return Err(fmt`Expected ${this}, but received ${Type.of(data)}: ${data}`); | |
} | |
const result = {} as Record<PropertyKey, Infer<V>>; | |
for (const key in data) { | |
const keyResult = this.keyType.validate(key); | |
if (!keyResult.success) { | |
return Err(fmt`Invalid key type for ${key}: ${keyResult.error}`); | |
} | |
const value = data[key as keyof typeof data]; | |
const valueResult = this.valueType.validate(value); | |
if (!valueResult.success) { | |
return Err(fmt`Invalid value for key "${key}": ${valueResult.error}`); | |
} | |
result[keyResult.data] = valueResult.data; | |
} | |
return Ok(result); | |
} | |
} | |
// #endregion Record | |
// #region Array | |
export class ArrayType<T extends Any> extends Type<Infer<T>[]> { | |
constructor(protected valueType: T) { | |
super(`Array<${valueType}>`); | |
Object.setPrototypeOf(this, ArrayType.prototype); | |
} | |
validate(data: unknown): Result<Infer<T>[]> { | |
if (!Array.isArray(data)) { | |
return Err(fmt`Expected ${this}, but received ${Type.of(data)}`); | |
} | |
const results: Infer<T>[] = []; | |
for (let i = 0; i < data.length; i++) { | |
const item = data[i]; | |
const result = this.valueType.validate(item); | |
if (!result.success) { | |
return Err(fmt`Invalid value at index ${i}: ${result.error}`); | |
} else { | |
results.push(result.data); | |
} | |
} | |
return Ok(results); | |
} | |
} | |
// #endregion Array | |
// #region Union | |
export class Union<U extends readonly Any[]> extends Type< | |
Infer<U[number]> | |
> { | |
static override [_metadata]: StaticMetadata<typeof Any> = { | |
...super[_metadata], | |
glue: "|", | |
}; | |
static override render<U extends readonly Any[]>(types: U): string { | |
return super.render(types); | |
} | |
constructor(protected readonly types: U) { | |
const name = Union.render(types); | |
super(name); | |
Object.setPrototypeOf(this, Union.prototype); | |
} | |
override get length(): number { | |
return this.types.length; | |
} | |
validate(data: unknown): Result<Infer<U[number]>> { | |
for (const subtype of this.types) { | |
const result = subtype.validate(data); | |
if (result.success) return result; | |
} | |
return Err(`Expected ${this}, but received ${Type.of(data)}`); | |
} | |
} | |
// #endregion Union | |
// #region Intersection | |
// deno-fmt-ignore | |
type UnionToIntersection<U> = ( | |
U extends unknown ? (u: U) => void : void | |
) extends ((i: infer I) => void) ? I : never; | |
type Intersect<U> = UnionToIntersection< | |
U extends readonly Any[] ? Infer<U[number]> : U | |
>; | |
export class Intersection<I extends readonly Any[]> extends Type<Intersect<I>> { | |
static override [_metadata]: StaticMetadata<typeof Any> = { | |
...super[_metadata], | |
glue: "&", | |
}; | |
constructor(protected readonly types: I) { | |
super(Intersection.render(types)); | |
Object.setPrototypeOf(this, Intersection.prototype); | |
} | |
override get length(): number { | |
return this.types.length; | |
} | |
validate(data: unknown): Result<Intersect<I>> { | |
for (const subtype of this.types) { | |
const result = subtype.validate(data); | |
if (result && !result.success) { | |
return Err<any>(result.error, { | |
value: data, | |
validator: subtype, | |
}); | |
} | |
} | |
return Ok<any>(data); | |
} | |
} | |
// #endregion Intersection | |
// #region InstanceOf | |
export class InstanceOf<T extends AbstractConstructor> | |
extends Type<InstanceType<T>> { | |
constructor(protected ctor: T) { | |
super(`InstanceOf<${ctor.name}>`); | |
} | |
validate(data: unknown): Result<InstanceType<T>> { | |
if (data != null) { | |
const test = this[_metadata].test ?? ((o: any) => o instanceof this.ctor); | |
if (test(data)) return Ok(data as any); | |
} | |
return Err( | |
fmt`Expected an instance of ${this.ctor}, but received ${data} (${ | |
Type.of(data) | |
})`, | |
{ | |
expected: this.name, | |
value: data, | |
validator: this, | |
}, | |
); | |
} | |
} | |
// #endregion InstanceOf | |
// #region Optional + Partial | |
export class Optional<T extends Any> extends Union<[T, Undefined]> { | |
constructor(protected type: T) { | |
super([type, undefined_]); | |
Object.setPrototypeOf(this, Optional.prototype); | |
} | |
} | |
// deno-lint-ignore ban-types | |
export class PartialType<T extends {} | Schema> | |
extends Type<Partial<Infer<T>>> { | |
/** | |
* Creates a new `Partial` validator for the given shape, making all keys in | |
* the object optional. | |
* | |
* If the `exact` flag is set to `true`, then the type will treat missing | |
* properties as distinct from those with `undefined` values (i.e. it will | |
* only allow its keys to be omitted, and will error if the key is present | |
* with a value of `undefined`). | |
* | |
* The `exact` flag will also cause any additional properties not present in | |
* the shape to error. | |
* | |
* @param shape The shape of the object to validate. | |
* @param [exact=false] Whether to treat missing properties as distinct from | |
* those with `undefined` values, and to error on additional properties. | |
*/ | |
constructor( | |
shape: T | SchemaType<T>, | |
protected readonly exact: boolean = false, | |
) { | |
if (shape instanceof PartialType) return shape as unknown as this; | |
const schema = shape instanceof SchemaType ? shape[_shape] : shape; | |
super(PartialType.render(schema, exact)); | |
Object.setPrototypeOf(this, PartialType.prototype); | |
} | |
protected get [_shape](): T { | |
return this[_metadata].schema as unknown as T; | |
} | |
validate(data: unknown): Result<Partial<Infer<T>>> { | |
if (typeof data !== "object" || data === null || Array.isArray(data)) { | |
return Err(fmt`Expected object, but received ${Type.of(data)}`); | |
} | |
const result = {} as Record<PropertyKey, any>; | |
for (const key in this[_shape]) { | |
const propValidator = this[_shape][key as keyof T]; | |
const value = (data as Record<PropertyKey, any>)[key]; | |
if (typeof value === "undefined") { | |
// skip undefined values for exact types | |
if (this.exact && !(key in data)) continue; | |
} else if (propValidator instanceof Type) { | |
const res = propValidator?.validate(value); | |
if (res && !res.success) { | |
return Err(fmt`Invalid value for key "${key}": ${res?.error}`); | |
} else if (res) { | |
result[key] = res.data; | |
} | |
} | |
} | |
return Ok<any>(result); | |
} | |
static override render<T extends Schema>( | |
schema: T | SchemaType<T>, | |
exact = false, | |
tag = "", | |
indentWidth = 2, | |
): string { | |
const indent = " ".repeat(indentWidth); | |
const nested = (v: any) => { | |
const s = String(v); | |
if (/\r?\n/g.test(s.trim())) return s.replace(/^(?=\s+\S)/gm, indent); | |
return s; | |
}; | |
const structure = Object.entries( | |
schema instanceof SchemaType | |
? schema[_shape] | |
: schema instanceof Type | |
? schema[_metadata].schema | |
: schema, | |
).reduce( | |
(acc, [k, v]) => | |
`${acc}${indent}${k}?: ${nested(v)}${exact ? "" : " | undefined"};\n`, | |
"{\n", | |
) + "}"; | |
return tag ? `${tag}<${structure}>` : structure; | |
} | |
} | |
// #endregion Partial | |
// #region PropertyKey | |
export class KeyType extends Union<[StringType, NumberType, SymbolType]> { | |
constructor() { | |
super([string, number, symbol]); | |
Object.setPrototypeOf(this, KeyType.prototype); | |
} | |
} | |
// #endregion PropertyKey | |
// #region Binary Data Structures | |
const ArrayBufferPrototype = globalThis.ArrayBuffer.prototype; | |
const ArrayBufferPrototypeGetByteLength = Object.getOwnPropertyDescriptor( | |
ArrayBufferPrototype, | |
"byteLength", | |
)?.get as (this: unknown) => number; | |
export class ArrayBufferType extends Type<ArrayBuffer> { | |
constructor() { | |
super("ArrayBuffer"); | |
Object.setPrototypeOf(this, ArrayBufferType.prototype); | |
} | |
validate(data: unknown): Result<ArrayBuffer> { | |
try { | |
ArrayBufferPrototypeGetByteLength.call(data); | |
return Ok(data as ArrayBuffer); | |
} catch (cause) { | |
return Err(fmt`Expected ArrayBuffer, but received ${Type.of(data)}`, { | |
cause, | |
}); | |
} | |
} | |
} | |
const DataViewPrototype = globalThis.DataView.prototype; | |
const DataViewPrototypeGetByteLength = Object.getOwnPropertyDescriptor( | |
DataViewPrototype, | |
"byteLength", | |
)?.get as (this: unknown) => number; | |
export class DataViewType extends Type<DataView> { | |
constructor() { | |
super("DataView"); | |
Object.setPrototypeOf(this, DataViewType.prototype); | |
} | |
validate(data: unknown): Result<DataView> { | |
let cause: unknown; | |
try { | |
DataViewPrototypeGetByteLength.call(data); | |
return Ok(data as DataView); | |
} catch (error) { | |
cause = error; | |
} | |
return Err(fmt`Expected DataView, but received ${Type.of(data)}`, { | |
cause, | |
}); | |
} | |
} | |
type TryInferGlobalType<K extends string | symbol, T = unknown> = | |
typeof globalThis extends { [P in K]: infer U } ? U extends T ? U : never | |
: never; | |
export type TypedArrayConstructor = | |
| Int8ArrayConstructor | |
| Int16ArrayConstructor | |
| Int32ArrayConstructor | |
| Uint8ArrayConstructor | |
| Uint8ClampedArrayConstructor | |
| Uint16ArrayConstructor | |
| Uint32ArrayConstructor | |
| Float32ArrayConstructor | |
| Float64ArrayConstructor | |
| TryInferGlobalType<"Float16Array"> | |
| TryInferGlobalType<"BigInt64Array"> | |
| TryInferGlobalType<"BigUint64Array">; | |
export type TypedArray = TypedArrayConstructor["prototype"]; | |
export type TypedArrayTypeName = TypedArray[typeof Symbol.toStringTag]; | |
const TypedArray: TypedArrayConstructor = Object.getPrototypeOf( | |
globalThis.Uint8Array, | |
); | |
const TypedArrayPrototype: TypedArray = TypedArray.prototype as TypedArray; | |
const TypedArrayPrototypeSymbolToStringTag = Object.getOwnPropertyDescriptor( | |
TypedArrayPrototype, | |
Symbol.toStringTag, | |
)?.get as (this: unknown) => TypedArrayTypeName | undefined; | |
export class TypedArrayType< | |
K extends TypedArrayTypeName, | |
T extends Extract<TypedArray, { [Symbol.toStringTag]: K }> = Extract< | |
TypedArray, | |
{ [Symbol.toStringTag]: K } | |
>, | |
> extends Type<T> { | |
constructor(override readonly name: K) { | |
super(name); | |
Object.setPrototypeOf(this, TypedArrayType.prototype); | |
} | |
validate(data: unknown): Result<T> { | |
let cause: unknown; | |
try { | |
const tag = TypedArrayPrototypeSymbolToStringTag.call(data); | |
if (tag === this.name) return Ok(data as T); | |
} catch (error) { | |
cause = error; | |
} | |
return Err( | |
fmt`Expected a typed array of type ${this.name}, but received ${data}`, | |
{ cause }, | |
); | |
} | |
} | |
// #endregion Binary Data Structures | |
// #region Type Instances and Factories | |
// #region Abstract Instances | |
export const any: Any = new Any(); | |
export const unknown: Unknown = new Unknown(); | |
export const never: Never = new Never(); | |
// #endregion Abstract Instances | |
// #region Primitive Instances | |
/** | |
* Represents the primitive `string` type, with validation support for both | |
* the type-level (compile time) and value level (runtime). | |
* | |
* @category Primitive | |
*/ | |
export const string: StringType = new StringType(); | |
/** | |
* Represents the primitive `number` type, with validation support for both | |
* the type-level (compile time) and value level (runtime). | |
* | |
* @category Primitive | |
*/ | |
export const number: NumberType = new NumberType(); | |
/** | |
* Represents the primitive `bigint` type, with validation support for both | |
* the type-level (compile time) and value level (runtime). | |
* | |
* @category Primitive | |
*/ | |
export const bigint: BigIntType = new BigIntType(); | |
/** | |
* Represents the primitive `symbol` type, with validation support for both | |
* the type-level (compile time) and value level (runtime). | |
* | |
* @category Primitive | |
*/ | |
export const symbol: SymbolType = new SymbolType(); | |
/** | |
* Represents the primitive `boolean` type, with validation support for both | |
* the type-level (compile time) and value level (runtime). | |
* | |
* @category Primitive | |
*/ | |
export const boolean: BooleanType = new BooleanType(); | |
/** | |
* Represents any non-null object type. | |
* | |
* @category Primitive | |
*/ | |
export const object: ObjectType = new ObjectType(); | |
/** | |
* Represents the built-in `PropertyKey` type in TypeScript, which is a union | |
* of `string`, `number`, and `symbol` primitives. This type is used to index | |
* objects and arrays, and supports validation on both the type-level and the | |
* value level. | |
* | |
* @category Primitive | |
*/ | |
export const propertyKey: KeyType = new KeyType(); | |
/** | |
* Represents the primitive `null` type, with validation support for both | |
* the type-level (compile time) and value level (runtime). | |
* | |
* @category Primitive | |
*/ | |
const null_: Null = new Null(); | |
/** | |
* Represents the primitive `undefined` type, with validation support for both | |
* the type-level (compile time) and value level (runtime). | |
* | |
* @category Primitive | |
*/ | |
const undefined_: Undefined = new Undefined(); | |
/** | |
* Represents the primitive `void` type, with validation support for both | |
* the type-level (compile time) and value level (runtime). | |
* | |
* @category Primitive | |
*/ | |
const void_: Void = new Void(); | |
/** | |
* Represents values that are either `null` or `undefined`. | |
* | |
* @category Primitive | |
*/ | |
export const nullish = union(null_, undefined_); | |
export { null_ as null, undefined_ as undefined, void_ as void }; | |
export function primitive<K extends PrimitiveTypeName>( | |
typeName: K, | |
): Primitive<PrimitiveTypeMap[K]> { | |
return new Primitive(typeName); | |
} | |
// #endregion Primitive Instances | |
export function union<U extends readonly Any[]>( | |
...types: U | |
): Union<U> { | |
return new Union(types); | |
} | |
export function intersection<I extends readonly Any[]>( | |
...types: I | |
): Intersection<I> { | |
return new Intersection(types); | |
} | |
export function literal<const T>(value: T): Literal<T> { | |
return new Literal(value); | |
} | |
export function instanceOf<T extends AbstractConstructor>( | |
constructor: T, | |
): InstanceOf<T> { | |
return new InstanceOf(constructor); | |
} | |
export function array<T extends Any>(type: T): ArrayType<T> { | |
return new ArrayType(type); | |
} | |
export function schema<T extends Schema>( | |
shape: T, | |
): SchemaType<T> { | |
return new SchemaType(shape); | |
} | |
export { schema as struct }; | |
export function optional<T extends Any>( | |
validator: T, | |
): Optional<T> { | |
return new Optional(validator); | |
} | |
export function partial<K extends PropKey, V extends Any>( | |
shape: RecordType<K, V>, | |
exact?: boolean, | |
): PartialType<RecordType<K, V>>; | |
export function partial<T extends Schema>( | |
shape: T, | |
exact?: boolean, | |
): PartialType<T>; | |
export function partial<T extends Schema>( | |
shape: T, | |
exact?: boolean, | |
): PartialType<T> { | |
return new PartialType(shape, exact); | |
} | |
export type PropKey = StringType | NumberType | SymbolType; | |
export function record<K extends PropKey, V extends Any>( | |
keyType: K, | |
valueType: V, | |
): RecordType<K, V> { | |
return new RecordType(keyType, valueType); | |
} | |
// #endregion Type Instances and Factories | |
// #region Examples | |
// const _config = schema({ | |
// name: string, | |
// version: string, | |
// description: optional(string), | |
// metadata: optional(record(string, any)), | |
// settings: partial(record(string, union(string, number, boolean))), | |
// }); | |
// const config: typeof _config = _config; | |
// type config = Infer<typeof config>; | |
// let obj: unknown = { | |
// name: "MyApp", | |
// version: "1.0.0", | |
// settings: { | |
// theme: "dark", | |
// } | |
// }; | |
// config.assert(obj); | |
// obj.name; | |
// #endregion Examples |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment