Skip to content

Instantly share code, notes, and snippets.

@nberlette
Last active March 14, 2025 21:12
Show Gist options
  • Save nberlette/cddab1da4e2a941bc8f7df7b32c11c44 to your computer and use it in GitHub Desktop.
Save nberlette/cddab1da4e2a941bc8f7df7b32c11c44 to your computer and use it in GitHub Desktop.
typescript runtime type validation
// 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();
}
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",
}));
// 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
// 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