Skip to content

Instantly share code, notes, and snippets.

@nberlette
Last active August 30, 2025 06:52
Show Gist options
  • Save nberlette/0d3573acef824ff08184b91312e6680a to your computer and use it in GitHub Desktop.
Save nberlette/0d3573acef824ff08184b91312e6680a to your computer and use it in GitHub Desktop.
another typescript runtime type library
// deno-lint-ignore-file no-explicit-any
import { _global } from "./primordials.ts";
/**
* Boolean indicating if the runtime is Bun.
*
* This is eagerly-evaluated at runtime.
* @see {@linkcode checkBun} for a lazy version.
*/
export const isBun = checkBun();
/**
* Boolean indicating if the runtime is Deno.
*
* This is eagerly-evaluated at runtime.
* @see {@linkcode checkDeno} for a lazy version.
*/
export const isDeno = checkDeno();
/**
* Boolean indicating if the runtime is Deno CLI.
*
* This is eagerly-evaluated at runtime.
* @see {@linkcode checkDenoCLI} for a lazy version.
*/
export const isDenoCLI = checkDenoCLI();
/**
* Boolean indicating if the runtime is Deno Deploy.
*
* This is eagerly-evaluated at runtime.
* @see {@linkcode checkDenoDeploy} for a lazy version.
*/
export const isDenoDeploy = checkDenoDeploy();
/**
* Boolean indicating if the runtime is Node.js.
*
* This is eagerly-evaluated at runtime.
* @see {@linkcode checkNode} for a lazy version.
*/
export const isNode = checkNode();
/**
* Boolean indicating if the runtime is Node.js or Bun.
*
* This is eagerly-evaluated at runtime.
* @see {@linkcode checkNodeLike} for a lazy version.
*/
export const isNodeLike = checkNodeLike();
/**
* Boolean indicating if the platform is windows.
*
* This is eagerly-evaluated at runtime.
* @see {@linkcode checkWindows} for a lazy version.
*/
export const isWindows = checkWindows();
// #endregion platform/runtime detection (eager evaluation)
// #region platform/runtime detection (lazy evaluation)
export function checkDeno(): boolean {
if (
typeof _global.navigator !== "undefined" &&
"userAgent" in _global.navigator
) {
return _global.navigator.userAgent?.includes("Deno");
} else {
return checkDenoNs();
}
}
export function checkDenoDeploy(): boolean {
return checkDenoNs() &&
_global.Deno.version.typescript === "" &&
_global.Deno.version.deno === "" &&
_global.Deno.version.v8 === "";
}
export function checkDenoNs(): boolean {
return typeof _global.Deno !== "undefined" &&
typeof _global.Deno.version === "object" &&
typeof _global.Deno.version.typescript === "string" &&
typeof _global.Deno.version.deno === "string" &&
typeof _global.Deno.version.v8 === "string";
}
export function checkDenoCLI(): boolean {
return checkDenoNs() && !checkDenoDeploy();
}
export function checkNodeLike(): boolean {
return typeof _global.process !== "undefined" &&
typeof _global.process.versions === "object" &&
!!_global.process.versions.modules &&
!!_global.process.versions.node;
}
/**
* @returns true if the runtime is Node.js, false otherwise.
*/
export function checkNode(): boolean {
return checkNodeLike() &&
typeof _global.process.getBuiltinModule === "function" &&
_global.process.getBuiltinModule("node:process") === _global.process;
}
export function checkNodeRequire(): boolean {
return checkNodeLike() &&
typeof _global.require === "function" &&
typeof _global.require.resolve === "function";
}
export function checkBun(): boolean {
return checkNodeLike() && "Bun" in _global &&
typeof (_global as any).Bun === "object" &&
"isBun" in _global.process &&
_global.process.isBun === true &&
typeof _global.process.versions.bun === "string";
}
/**
* @returns true if the platform is Windows, false otherwise.
*/
export function checkWindows(): boolean {
if (
typeof navigator !== "undefined" && "platform" in navigator &&
typeof navigator.platform === "string"
) return navigator.platform.toLowerCase().startsWith("win");
if (
typeof _global.process !== "undefined" &&
"platform" in _global.process
) return _global.process.platform === "win32";
return false;
}
/**
* @returns The Node.js `fs` module.
*/
export function getNodeFs(): typeof import("node:fs") {
if (
isNodeLike &&
typeof _global.process.getBuiltinModule === "undefined" &&
typeof _global.require === "function"
) {
return _global.require("node:fs");
}
return _global.process.getBuiltinModule("node:fs");
}
/**
* @returns The Node.js `os` module.
*/
export function getNodeOs(): typeof import("node:os") {
if (
isNodeLike &&
typeof _global.process.getBuiltinModule === "undefined" &&
typeof _global.require === "function"
) {
return _global.require("node:os");
}
return _global.process.getBuiltinModule("node:os");
}
/**
* @returns The Node.js `path` module.
*/
export function getNodePath(): typeof import("node:path") {
if (
isNodeLike &&
typeof _global.process.getBuiltinModule === "undefined" &&
typeof _global.require === "function"
) {
return _global.require("node:path");
}
return _global.process.getBuiltinModule("node:path");
}
/**
* Checks if the current environment has any indicators that colors should be
* disabled in TTY/console output.
*
* If the environment is a Node-like runtime (Deno, Node, Bun), this will check
* several different variables in the `process.env` object. If the runtime is
* Deno, it will also consult the `Deno.noColor` property.
*
* If the environment is a browser, this will check the `navigator` object for
* the `userAgent` string. If the environment is neither, it will return false.
*
* @returns a boolean indicating whether or not colors are disabled.
*/
export function getNoColor(): boolean {
if (isDeno && _global.Deno.noColor) return true;
if (isNodeLike || isBun) {
const env = _global.process.env ?? {};
return (
(!!env.NO_COLOR && env.NO_COLOR !== "0") ||
env.FORCE_COLOR === "0" ||
env.CLICOLOR === "0" ||
env.DISABLE_COLORS === "1" ||
env.DISABLE_COLORS === "true" ||
env.NODE_DISABLE_COLORS === "1"
);
}
if (
typeof _global.navigator !== "undefined" &&
"userAgent" in _global.navigator
) {
const ua = _global.navigator.userAgent;
return (
ua.includes("IE") ||
ua.includes("Trident") ||
ua.includes("Edge") ||
ua.includes("Phone") ||
ua.includes("BlackBerry") ||
ua.includes("Opera") ||
ua.includes("Mobile") ||
ua.includes("Android") ||
ua.includes("webOS") ||
ua.includes("Kindle") ||
ua.includes("Silk") ||
ua.includes("PlayBook") ||
ua.includes("Tablet") ||
ua.includes("Nook") ||
ua.includes("Fennec")
);
}
return false;
}
// todo(nberlette): remove this file... whenever gistpad decides to allow it
export * from "./primordials.ts";
export * from "jsr:@nick/[email protected]/printable";
export * from "jsr:@nick/[email protected]/primitive";
export * from "jsr:@nick/[email protected]/null";
export * from "jsr:@nick/[email protected]/undefined";
export * from "jsr:@nick/[email protected]/missing";
export * from "jsr:@nick/[email protected]/string";
export * from "jsr:@nick/[email protected]/string-object";
export * from "jsr:@nick/[email protected]/number";
export * from "jsr:@nick/[email protected]/number-object";
export * from "jsr:@nick/[email protected]/symbol";
export * from "jsr:@nick/[email protected]/symbol-object";
export * from "jsr:@nick/[email protected]/bigint";
export * from "jsr:@nick/[email protected]/bigint-object";
export * from "jsr:@nick/[email protected]/boolean";
export * from "jsr:@nick/[email protected]/boolean-object";
export * from "jsr:@nick/[email protected]/boxed-primitive";
export * from "jsr:@nick/[email protected]/object";
export * from "jsr:@nick/[email protected]/function";
export * from "jsr:@nick/[email protected]/wellknown-symbol";
export * from "jsr:@nick/[email protected]/registered-symbol";
export * from "jsr:@nick/[email protected]/unique-symbol";
export * from "jsr:@nick/[email protected]/property-key";
export * from "jsr:@nick/[email protected]/array";
export * from "jsr:@nick/[email protected]/array-like";
export * from "jsr:@nick/[email protected]/template-object";
export * from "jsr:@nick/[email protected]/template-strings-array";
export * from "jsr:@nick/[email protected]/plain-object";
export * from "jsr:@nick/[email protected]/regexp";
export * from "jsr:@nick/[email protected]/error";
export * from "jsr:@nick/[email protected]/promise";
export * from "jsr:@nick/[email protected]/map";
export * from "jsr:@nick/[email protected]/set";
export * from "jsr:@nick/[email protected]/weak-key";
export * from "jsr:@nick/[email protected]/weak-map";
export * from "jsr:@nick/[email protected]/weak-set";
export * from "jsr:@nick/[email protected]/weak-ref";
export * from "jsr:@nick/[email protected]/whitespace";
export * from "jsr:@nick/[email protected]/array-buffer";
export * from "jsr:@nick/[email protected]/array-buffer-like";
export * from "jsr:@nick/[email protected]/array-buffer-view";
export * from "jsr:@nick/[email protected]/shared-array-buffer";
export * from "jsr:@nick/[email protected]/data-view";
export * from "jsr:@nick/[email protected]/typed-array";
export * from "jsr:@nick/[email protected]/int8-array";
export * from "jsr:@nick/[email protected]/uint8-array";
export * from "jsr:@nick/[email protected]/uint8-clamped-array";
export * from "jsr:@nick/[email protected]/int16-array";
export * from "jsr:@nick/[email protected]/uint16-array";
export * from "jsr:@nick/[email protected]/int32-array";
export * from "jsr:@nick/[email protected]/uint32-array";
export * from "jsr:@nick/[email protected]/float16-array";
export * from "jsr:@nick/[email protected]/float32-array";
export * from "jsr:@nick/[email protected]/float64-array";
export * from "jsr:@nick/[email protected]/bigint64-array";
export * from "jsr:@nick/[email protected]/biguint64-array";
export * from "jsr:@nick/[email protected]/iterable";
export * from "jsr:@nick/[email protected]/iterable-object";
export * from "jsr:@nick/[email protected]/iterator";
export * from "jsr:@nick/[email protected]/iterable-iterator";
export * from "jsr:@nick/[email protected]/generator";
export * from "jsr:@nick/[email protected]/generator-function";
export * from "jsr:@nick/[email protected]/async-iterable";
export * from "jsr:@nick/[email protected]/async-iterable-object";
export * from "jsr:@nick/[email protected]/async-iterator";
export * from "jsr:@nick/[email protected]/async-iterable-iterator";
export * from "jsr:@nick/[email protected]/async-generator";
export * from "jsr:@nick/[email protected]/async-generator-function";
export * from "jsr:@nick/[email protected]/async-function";
export * from "jsr:@nick/[email protected]/async-disposable";
export * from "jsr:@nick/[email protected]/disposable"; // gosh I hope not
export * from "jsr:@nick/[email protected]/url-string";
export * from "jsr:@nick/[email protected]/url";
export * from "jsr:@nick/[email protected]/url-search-params";
export * from "jsr:@nick/[email protected]/whitespace";
export * from "jsr:@nick/[email protected]/writable-stream";
export * from "jsr:@nick/[email protected]/readable-stream";
export * from "jsr:@nick/[email protected]/writer";
export * from "jsr:@nick/[email protected]/writer/sync";
export * from "jsr:@nick/[email protected]/reader";
export * from "jsr:@nick/[email protected]/reader/sync";
export * from "jsr:@nick/[email protected]/semver";
export * from "jsr:@nick/[email protected]/date";
export * from "jsr:@nick/[email protected]/date-string";
export * from "./runtime_types.ts";
export * as default from "./runtime_types.ts";
MIT License
Copyright (c) 2024-2025+ Nicholas Berlette (https://github.com/nberlette)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// deno-lint-ignore-file no-explicit-any no-var
// #region primordials
// FIXME(nberlette): this is a hacky workaround for the fact that Deno still
// doesn't seem to consistently support the global Node types. some times they
// work fine, other times they end up resolving to `any`. but importing the
// @types/node like this seems to ensure the types are always there. *shrug*
type _ = import("https://esm.sh/@types/[email protected]/index.d.ts");
declare const global: typeof globalThis | undefined;
declare const root: typeof globalThis | undefined;
declare const window: typeof globalThis | undefined;
declare const self: typeof globalThis | undefined;
export var _global: _global = (() => {
// most environments will take this branch
if (typeof globalThis !== "undefined") return globalThis;
// node
if (typeof global !== "undefined") return global;
// browsers, deno v1
if (typeof window !== "undefined") return window;
// web workers
if (typeof self !== "undefined") return self;
// old browsers (which ones?)
if (typeof root !== "undefined") return root;
// non-strict mode (cjs)
if (typeof this !== "undefined") return this!;
// this will throw if `--disable-code-generation-from-strings` is set...
// which is fine, since we would probably want to throw an error after
// this point anyway ;)
return (0, eval)("this");
})();
export type _global = typeof globalThis & {
readonly process: typeof import("node:process");
readonly require: ReturnType<typeof import("node:module").createRequire>;
readonly Buffer: typeof import("node:buffer").Buffer;
};
export var Set: SetConstructor = _global.Set;
export var SetPrototype: Set<any> = Set.prototype;
export type Set<T> = globalThis.Set<T>;
export type SetConstructor = globalThis.SetConstructor;
export var Map: MapConstructor = _global.Map;
export var MapPrototype: Map<any, any> = Map.prototype;
export type Map<K, V> = globalThis.Map<K, V>;
export type MapConstructor = globalThis.MapConstructor;
export var WeakMap: WeakMapConstructor = _global.WeakMap;
export var WeakMapPrototype: WeakMap<WeakKey, any> = WeakMap.prototype;
export type WeakMap<K extends WeakKey, V> = globalThis.WeakMap<K, V>;
export type WeakMapConstructor = globalThis.WeakMapConstructor;
export var WeakSet: WeakSetConstructor = _global.WeakSet;
export var WeakSetPrototype: WeakSet<WeakKey> = WeakSet.prototype;
export type WeakSet<T extends WeakKey> = globalThis.WeakSet<T>;
export type WeakSetConstructor = globalThis.WeakSetConstructor;
export var WeakRef: WeakRefConstructor = _global.WeakRef;
export var WeakRefPrototype: WeakRef<WeakKey> = WeakRef.prototype;
export type WeakRef<T extends WeakKey> = globalThis.WeakRef<T>;
export type WeakRefConstructor = globalThis.WeakRefConstructor;
export var FinalizationRegistry: FinalizationRegistryConstructor =
_global.FinalizationRegistry;
export type FinalizationRegistry<T> = globalThis.FinalizationRegistry<T>;
export type FinalizationRegistryConstructor =
globalThis.FinalizationRegistryConstructor;
export var Proxy: ProxyConstructor = _global.Proxy;
export type ProxyHandler<T extends object> = globalThis.ProxyHandler<T>;
export type ProxyConstructor = globalThis.ProxyConstructor;
export var Reflect = _global.Reflect;
export var ReflectGet = Reflect.get;
export var ReflectHas = Reflect.has;
export var ReflectOwnKeys = Reflect.ownKeys;
export const Symbol: SymbolConstructor = _global.Symbol;
export type Symbol = globalThis.Symbol;
export type SymbolConstructor = globalThis.SymbolConstructor;
export const SymbolFor: typeof Symbol.for = Symbol.for;
export const SymbolKeyFor: typeof Symbol.keyFor = Symbol.keyFor;
// deno-fmt-ignore
export const SymbolIsConcatSpreadable: SymbolIsConcatSpreadable = Symbol.isConcatSpreadable;
export type SymbolIsConcatSpreadable = typeof Symbol.isConcatSpreadable;
export const SymbolIterator: SymbolIterator = Symbol.iterator;
export type SymbolIterator = typeof Symbol.iterator;
export const SymbolAsyncIterator: SymbolAsyncIterator = Symbol.asyncIterator;
export type SymbolAsyncIterator = typeof Symbol.asyncIterator;
export const SymbolHasInstance: SymbolHasInstance = Symbol.hasInstance;
export type SymbolHasInstance = typeof Symbol.hasInstance;
export const SymbolToStringTag: SymbolToStringTag = Symbol.toStringTag;
export type SymbolToStringTag = typeof Symbol.toStringTag;
export const SymbolDispose: SymbolDispose = Symbol.dispose;
export type SymbolDispose = typeof Symbol.dispose;
export const SymbolAsyncDispose: SymbolAsyncDispose = Symbol.asyncDispose;
export type SymbolAsyncDispose = typeof Symbol.asyncDispose;
export const SymbolMetadata: SymbolMetadata = Symbol.metadata;
export type SymbolMetadata = typeof Symbol.metadata;
export const SymbolMatch: SymbolMatch = Symbol.match;
export type SymbolMatch = typeof Symbol.match;
export const SymbolMatchAll: SymbolMatchAll = Symbol.matchAll;
export type SymbolMatchAll = typeof Symbol.matchAll;
export const SymbolReplace: SymbolReplace = Symbol.replace;
export type SymbolReplace = typeof Symbol.replace;
export const SymbolSearch: SymbolSearch = Symbol.search;
export type SymbolSearch = typeof Symbol.search;
export const SymbolSpecies: SymbolSpecies = Symbol.species;
export type SymbolSpecies = typeof Symbol.species;
export const SymbolSplit: SymbolSplit = Symbol.split;
export type SymbolSplit = typeof Symbol.split;
export const SymbolToPrimitive: SymbolToPrimitive = Symbol.toPrimitive;
export type SymbolToPrimitive = typeof Symbol.toPrimitive;
export const SymbolUnscopables: SymbolUnscopables = Symbol.unscopables;
export type SymbolUnscopables = typeof Symbol.unscopables;
export var Function: FunctionConstructor = _global.Function;
export var FunctionPrototype: Function = Function.prototype;
export type Function = globalThis.Function;
export type FunctionConstructor = globalThis.FunctionConstructor;
export var { bind, call, apply } = FunctionPrototype;
/**
* @template T
* @template {readonly unknown[]} [A=any[]]
* @template [R=any]
*/
export var uncurryThis: <T, A extends readonly unknown[] = any[], R = any>(
ƒ: (this: T, ...a: A) => R,
self?: T,
) => (self: T, ...args: A) => R = bind.bind(call);
export const FunctionPrototypeBind: <T, A extends any[], B extends any[], R>(
self: (this: T, ...args: [...A, ...B]) => R,
thisArg: T,
...args: A
) => (...args: B) => R = uncurryThis(bind);
export const FunctionPrototypeCall: <T, A extends any[], R>(
self: (this: T, ...args: A) => R,
thisArg: T,
...args: A
) => R = uncurryThis(call as CallableFunction["call"]);
export const FunctionPrototypeApply: <T, A extends any[], R>(
self: (this: T, ...args: A) => R,
thisArg: T,
args: A,
) => R = uncurryThis(apply);
export var Object: ObjectConstructor = _global.Object;
export type Object = globalThis.Object;
export type ObjectConstructor = globalThis.ObjectConstructor;
export var ObjectPrototype: Object = Object.prototype;
export var ObjectPrototypeToString: (self: unknown) => string = uncurryThis(
ObjectPrototype.toString,
);
export var ObjectPrototypeHasOwnProperty: ObjectHasOwn = uncurryThis(
ObjectPrototype.hasOwnProperty,
) as ObjectHasOwn;
export var ObjectCreate: typeof Object.create = Object.create;
export var ObjectDefineProperty: typeof Object.defineProperty =
Object.defineProperty;
export var ObjectFreeze: typeof Object.freeze = Object.freeze;
export var ObjectKeys: <T extends object>(
o: T,
) => ((string | number) & keyof T)[] = Object.keys;
export var ObjectValues: <T extends object>(o: T) => T[keyof T][] =
Object.values;
export var ObjectEntries: <T extends object>(o: T) => {
[K in keyof T]: [key: K, value: T[K]];
}[keyof T][] = Object.entries;
export var ObjectGetOwnPropertyDescriptor:
typeof Object.getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
export var ObjectGetOwnPropertyDescriptors:
typeof Object.getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors;
export var ObjectGetOwnPropertyNames: typeof Object.getOwnPropertyNames =
Object.getOwnPropertyNames;
export var ObjectGetOwnPropertySymbols: typeof Object.getOwnPropertySymbols =
Object.getOwnPropertySymbols;
export var ObjectGetPrototypeOf: typeof Object.getPrototypeOf =
Object.getPrototypeOf;
export var ObjectSetPrototypeOf: typeof Object.setPrototypeOf =
Object.setPrototypeOf;
export var ObjectDefineProperties = Object.defineProperties || function (o, p) {
for (const k in p) {
if (ObjectHasOwn(p, k)) ObjectDefineProperty(o, k, p[k]);
}
return o;
};
export var ObjectIs: typeof Object.is = Object.is || function (a, b) {
if (a === b) return a !== 0 || 1 / a === 1 / b;
return a !== a && b !== b;
};
export var ObjectHasOwn: ObjectHasOwn = (() => {
if (typeof Object.hasOwn === "function") return Object.hasOwn as ObjectHasOwn;
return ObjectPrototypeHasOwnProperty;
})();
export type ObjectHasOwn = {
<T extends object, K extends PropertyKey = keyof T>(
o: T,
p: K,
): o is T & Record<K, K extends keyof T ? T[K] : unknown>;
(o: object, p: PropertyKey): boolean;
};
export var Error: ErrorConstructor = _global.Error;
export type Error = globalThis.Error;
export type ErrorConstructor = globalThis.ErrorConstructor;
export var TypeError: TypeErrorConstructor = _global.TypeError;
export type TypeError = globalThis.TypeError;
export type TypeErrorConstructor = globalThis.TypeErrorConstructor;
export var RangeError: RangeErrorConstructor = _global.RangeError;
export type RangeError = globalThis.RangeError;
export type RangeErrorConstructor = globalThis.RangeErrorConstructor;
export var ReferenceError: ReferenceErrorConstructor = _global.ReferenceError;
export type ReferenceError = globalThis.ReferenceError;
export type ReferenceErrorConstructor = globalThis.ReferenceErrorConstructor;
export var SyntaxError: SyntaxErrorConstructor = _global.SyntaxError;
export type SyntaxError = globalThis.SyntaxError;
export type SyntaxErrorConstructor = globalThis.SyntaxErrorConstructor;
export var URIError: URIErrorConstructor = _global.URIError;
export type URIError = globalThis.URIError;
export type URIErrorConstructor = globalThis.URIErrorConstructor;
export var EvalError: EvalErrorConstructor = _global.EvalError;
export type EvalError = globalThis.EvalError;
export type EvalErrorConstructor = globalThis.EvalErrorConstructor;
export var AggregateError: AggregateErrorConstructor = _global.AggregateError;
export type AggregateError = globalThis.AggregateError;
export type AggregateErrorConstructor = globalThis.AggregateErrorConstructor;
export var DOMException: typeof globalThis.DOMException = _global.DOMException;
export type DOMException = globalThis.DOMException;
export var Date: DateConstructor = _global.Date;
export type Date = globalThis.Date;
export type DateConstructor = globalThis.DateConstructor;
export var Array: ArrayConstructor = _global.Array;
export type Array<T> = globalThis.Array<T>;
export type ArrayConstructor = globalThis.ArrayConstructor;
export var Math = _global.Math;
export type Math = globalThis.Math;
export var String: StringConstructor = _global.String;
export type String = globalThis.String;
export type StringConstructor = globalThis.StringConstructor;
export var Number: NumberConstructor = _global.Number;
export type Number = globalThis.Number;
export type NumberConstructor = globalThis.NumberConstructor;
export var Boolean: BooleanConstructor = _global.Boolean;
export type Boolean = globalThis.Boolean;
export type BooleanConstructor = globalThis.BooleanConstructor;
export var BigInt: BigIntConstructor = _global.BigInt;
export type BigInt = globalThis.BigInt;
export type BigIntConstructor = globalThis.BigIntConstructor;
export var JSON = _global.JSON;
export type JSON = globalThis.JSON;
export var DataView: DataViewConstructor = _global.DataView;
export var DataViewPrototype: DataView = DataView.prototype;
export type DataView<T extends ArrayBufferLike = ArrayBufferLike> =
globalThis.DataView<T>;
export type DataViewConstructor = globalThis.DataViewConstructor;
export var ArrayBuffer: ArrayBufferConstructor = _global.ArrayBuffer;
export var ArrayBufferPrototype: ArrayBuffer = ArrayBuffer.prototype;
export type ArrayBuffer = globalThis.ArrayBuffer;
export type ArrayBufferConstructor = globalThis.ArrayBufferConstructor;
export var Int8Array: Int8ArrayConstructor = _global.Int8Array;
export var Int8ArrayPrototype: Int8Array = Int8Array.prototype;
export type Int8Array<T extends ArrayBufferLike = ArrayBufferLike> =
globalThis.Int8Array<T>;
export type Int8ArrayConstructor = globalThis.Int8ArrayConstructor;
export var Int16Array: Int16ArrayConstructor = _global.Int16Array;
export var Int16ArrayPrototype: Int16Array = Int16Array.prototype;
export type Int16Array<T extends ArrayBufferLike = ArrayBufferLike> =
globalThis.Int16Array<T>;
export type Int16ArrayConstructor = globalThis.Int16ArrayConstructor;
export var Int32Array: Int32ArrayConstructor = _global.Int32Array;
export var Int32ArrayPrototype: Int32Array = Int32Array.prototype;
export type Int32Array<T extends ArrayBufferLike = ArrayBufferLike> =
globalThis.Int32Array<T>;
export type Int32ArrayConstructor = globalThis.Int32ArrayConstructor;
export var Float16Array: Float16ArrayConstructor = _global.Float16Array;
export var Float16ArrayPrototype: Float16Array = Float16Array.prototype;
export type Float16Array<T extends ArrayBufferLike = ArrayBufferLike> =
globalThis.Float16Array<T>;
export type Float16ArrayConstructor = globalThis.Float16ArrayConstructor;
export var Float32Array: Float32ArrayConstructor = _global.Float32Array;
export var Float32ArrayPrototype: Float32Array = Float32Array.prototype;
export type Float32Array<T extends ArrayBufferLike = ArrayBufferLike> =
globalThis.Float32Array<T>;
export type Float32ArrayConstructor = globalThis.Float32ArrayConstructor;
export var Float64Array: Float64ArrayConstructor = _global.Float64Array;
export var Float64ArrayPrototype: Float64Array = Float64Array.prototype;
export type Float64Array<T extends ArrayBufferLike = ArrayBufferLike> =
globalThis.Float64Array<T>;
export type Float64ArrayConstructor = globalThis.Float64ArrayConstructor;
export var Uint8Array: Uint8ArrayConstructor = _global.Uint8Array;
export var Uint8ArrayPrototype: Uint8Array = Uint8Array.prototype;
export type Uint8Array<T extends ArrayBufferLike = ArrayBufferLike> =
globalThis.Uint8Array<T>;
export type Uint8ArrayConstructor = globalThis.Uint8ArrayConstructor;
export var Uint8ClampedArray: Uint8ClampedArrayConstructor =
_global.Uint8ClampedArray;
export var Uint8ClampedArrayPrototype: Uint8ClampedArray =
Uint8ClampedArray.prototype;
export type Uint8ClampedArray<T extends ArrayBufferLike = ArrayBufferLike> =
globalThis.Uint8ClampedArray<T>;
export type Uint8ClampedArrayConstructor =
globalThis.Uint8ClampedArrayConstructor;
export var Uint16Array: Uint16ArrayConstructor = _global.Uint16Array;
export var Uint16ArrayPrototype: Uint16Array = Uint16Array.prototype;
export type Uint16Array<T extends ArrayBufferLike = ArrayBufferLike> =
globalThis.Uint16Array<T>;
export type Uint16ArrayConstructor = globalThis.Uint16ArrayConstructor;
export var Uint32Array: Uint32ArrayConstructor = _global.Uint32Array;
export var Uint32ArrayPrototype: Uint32Array = Uint32Array.prototype;
export type Uint32Array<T extends ArrayBufferLike = ArrayBufferLike> =
globalThis.Uint32Array<T>;
export type Uint32ArrayConstructor = globalThis.Uint32ArrayConstructor;
export var BigInt64Array: BigInt64ArrayConstructor = _global.BigInt64Array;
export var BigInt64ArrayPrototype: BigInt64Array = BigInt64Array.prototype;
export type BigInt64Array<T extends ArrayBufferLike = ArrayBufferLike> =
globalThis.BigInt64Array<T>;
export type BigInt64ArrayConstructor = globalThis.BigInt64ArrayConstructor;
export var BigUint64Array: BigUint64ArrayConstructor = _global.BigUint64Array;
export var BigUint64ArrayPrototype: BigUint64Array = BigUint64Array.prototype;
export type BigUint64Array<T extends ArrayBufferLike = ArrayBufferLike> =
globalThis.BigUint64Array<T>;
export type BigUint64ArrayConstructor = globalThis.BigUint64ArrayConstructor;
// #endregion primordials
// deno-lint-ignore-file no-explicit-any ban-types no-unused-vars
import { inspect, type InspectOptionsStylized } from "node:util";
import { lru } from "jsr:@decorators/lru";
import { alias } from "jsr:@decorators/alias";
import {
isArray,
isArrayBuffer,
isAsyncFunction,
isAsyncGenerator,
isAsyncGeneratorFunction,
isAsyncIterable,
isAsyncIterableIterator,
isAsyncIterator,
isBigInt64Array,
isBigUint64Array,
isDataView,
isDate,
isDateString,
isError,
isFinite,
isFloat,
isFloat16Array,
isFloat32Array,
isFloat64Array,
isFunction,
isGenerator,
isGeneratorFunction,
isInt16Array,
isInt32Array,
isInt8Array,
isInteger,
isIterable,
isIterableIterator,
isMap,
isNegative,
isNonZero,
isNull,
isObject,
isPlainObject,
isPositive,
isPrimitive,
isPromise,
isPropertyKey,
isRegExp,
isRegisteredSymbol,
isSet,
isString,
isTemplateStringsArray,
isTypedArray,
isUint16Array,
isUint32Array,
isUint8Array,
isUint8ClampedArray,
isUniqueSymbol,
isURLString,
isWeakKey,
isWeakMap,
isWeakRef,
isWeakSet,
isWellKnownSymbol,
type RegisteredSymbol,
type TemplateStringsArray,
type TypedArrayTypeMap,
type TypedArrayTypeName,
type UniqueSymbol,
type WellKnownSymbol,
} from "./guards.ts";
import {
FunctionPrototype,
FunctionPrototypeApply,
FunctionPrototypeCall,
ObjectDefineProperties,
ObjectFreeze,
ObjectGetOwnPropertyDescriptors,
ObjectSetPrototypeOf,
Proxy,
ReflectGet,
ReflectHas,
ReflectOwnKeys,
SymbolAsyncDispose,
SymbolAsyncIterator,
SymbolDispose,
SymbolHasInstance,
SymbolIsConcatSpreadable,
SymbolIterator,
SymbolMatch,
SymbolMatchAll,
SymbolMetadata,
SymbolReplace,
SymbolSearch,
SymbolSpecies,
SymbolSplit,
SymbolToPrimitive,
SymbolToStringTag,
SymbolUnscopables,
} from "./primordials.ts";
import { getNoColor } from "./env.ts";
type strings = string & {};
export interface RenderOptions {
indent?: number;
indentWidth?: number;
lineWidth?: number;
useTabs?: boolean;
trailingComma?: boolean;
semiColons?: boolean;
singleQuote?: boolean;
}
const defaultRenderOptions = {
indent: 0,
indentWidth: 2,
lineWidth: 80,
useTabs: false,
semiColons: true,
trailingComma: true,
singleQuote: false,
} as const satisfies RenderOptions;
type Id<T> = T;
class Callable<
This = unknown,
Args extends readonly unknown[] = any[],
Return = any,
> {
constructor(
factory: (this: This, ...args: Args) => Return,
) {
function fn(this: This, ...args: Args): Return {
return FunctionPrototypeCall(factory, this, ...args);
}
const proto = new Proxy(FunctionPrototype, {
getPrototypeOf: () => new.target.prototype,
setPrototypeOf: () => true,
get: (t, p, r) => {
if (p === "constructor") return new.target;
if (p === "prototype") return t.prototype;
if (p in new.target.prototype) {
return ReflectGet(new.target.prototype, p, r);
}
if (p === "name") return new.target.name;
return ReflectGet(t, p, r);
},
has: (t, p) => ReflectHas(new.target.prototype, p) || ReflectHas(t, p),
ownKeys: (
t,
) => [
...new Set(
ReflectOwnKeys(t).concat(ReflectOwnKeys(new.target.prototype)),
),
],
});
ObjectSetPrototypeOf(fn, proto);
ObjectDefineProperties(fn, ObjectGetOwnPropertyDescriptors(this));
return fn as never;
}
static {
ObjectSetPrototypeOf(this, Function);
}
}
const { colors, styles } = inspect;
const colorize = <T extends keyof typeof colors>(val: string, color: T) => {
const [before, after] = colors[color] ?? [];
if (before != null && after != null) {
return `\x1b[${before}m${val}\x1b[${after}m`;
} else {
return val;
}
};
const stylize: InspectOptionsStylized["stylize"] = (text, style) => {
if (style in styles) return colorize(text, styles[style]);
return text;
};
const defaultInspectOptions = {
colors: !getNoColor(),
compact: true,
breakLength: 100,
getters: "get",
maxArrayLength: 10,
maxStringLength: 60,
sorted: true,
numericSeparator: true,
customInspect: true,
showHidden: false,
showProxy: false,
depth: 0,
stylize,
} satisfies InspectOptionsStylized;
// #endregion internal
export interface Err<E = string> {
readonly success: false;
readonly error: E;
readonly path: PropertyKey[];
readonly errors?: readonly ValidationError[] | undefined;
}
export interface Ok<T = any> {
readonly success: true;
readonly value: T;
}
export type Result<T, E = string> = Ok<T> | Err<E>;
export interface ValidationErrorOptions<T = any, E = string> {
cause?: unknown;
error?: E;
errors?: (E | ValidationError<T, E>)[] | undefined;
path?: PropertyKey[];
type?: Type<T> | null;
}
type ResolvedValidationErrorOptions<T, E> = Required<
ValidationErrorOptions<T, E>
>;
// deno-fmt-ignore
const unicode = {
TL: "┌", TH: "─", TM: "┬", TR: "┐",
ML: "├", MH: "─", MM: "┼", MR: "┤",
LL: "│", LH: " ", LM: "├", LR: "│",
BL: "└", BH: "─", BM: "┴", BR: "┘",
V0: "╵", V2: "╎", V3: "┆", V4: "┊",
H0: "╴", H2: "╌", H3: "┄", H4: "┈",
} as const;
export class ValidationError<T = any, E = any> extends Error implements Err<E> {
static from<T, E = any>(
error: E,
options?: ValidationErrorOptions<T, E>,
): ValidationError<T, E>;
static from<T, E = any>(
error: ValidationError<T, E> | { error: E; path: PropertyKey[] },
options?: ValidationErrorOptions<T, E>,
): ValidationError<T, E>;
static from<T, E = any>(
options: ValidationErrorOptions<T, E>,
): ValidationError<T, E>;
static from(
error: any,
options?: ValidationErrorOptions<any, any>,
): ValidationError<any, any> {
if (error instanceof ValidationError) {
return new ValidationError(error.error, {
...error.#options,
...options,
cause: error,
});
}
if (error && typeof error === "object") {
const { error: e, errors = [], path = [], type = null } = error;
return new ValidationError(e, { ...options, path, type, errors });
}
return new ValidationError(error, options);
}
#options = {} as ResolvedValidationErrorOptions<T, E>;
constructor(
message: E,
options: ValidationErrorOptions<T, E> = {},
) {
const { cause, type = null, error = message, errors = [], path = [] } =
options;
super(error + "", { cause });
this.name = "ValidationError";
this.#options = { cause, error, errors, path, type };
}
get success(): false {
return false;
}
get error(): E {
return this.#options.error!;
}
get errors(): ValidationError<T, E>[] {
return this.#options.errors?.map((e) =>
ValidationError.from<T, E>(e as E)
) ?? [];
}
get path(): PropertyKey[] {
return this.#options.path ??= [];
}
get type(): Type<T> | null {
return this.#options.type!;
}
override toString(): string {
const { cause, error, errors = [], type, path = ["<root>"] } =
this.#options;
let msg = "";
if (error) msg += String(error);
if (path.length) {
msg += `\n \x1b[0;2;3mat \x1b[4;58;2;${0x44};${0x22};${0x6D}m${
path.join(".")
}\x1b[0m`;
}
if (type && type instanceof Type) {
msg +=
`\n \x1b[0;1;4;58;5;54mexpected type\x1b[0;2m:\x1b[0m ${type.name}`;
}
if (cause && isError(cause) && cause !== this) {
msg += "\n \x1b[0;1;4;58;5;93mcaused by\x1b[0;2m:\x1b[0m";
}
if (errors.length) {
msg += "\n \x1b[0;1;4;58;5;208mvalidation errors\x1b[0;2m:\x1b[0m";
let before = "", after = "";
for (let i = 0; i < errors.length; i++) {
const e = errors[i];
const indent = " ";
let text = "";
let guide = `${indent}${unicode.LL} `;
const lines = String(e).split(/\r?\n/g);
for (let j = 0; j < lines.length; j++) {
const line = lines[j];
// use a guide with a branch to the right (first line only)
if (j === 0 && i === 0) {
guide = `${indent}${unicode.ML}${unicode.H0}`;
before += `${indent}${unicode.TL}${
unicode.TH.repeat(94)
}${unicode.H2}${unicode.H3}${unicode.H4}\n`;
} else if (j === lines.length - 1 && i === errors.length - 1) {
guide = `${indent}${unicode.ML}${unicode.H0}`;
after += `${indent}${unicode.BL}${
unicode.BH.repeat(94)
}${unicode.H2}${unicode.H3}${unicode.H4}\n`;
} else {
guide = `${indent}${unicode.LL} `;
}
text += line.replace(/^/mg, `\x1b[2m${guide}\x1b[0m`) + "\n";
}
msg += `\n${before}${text}${after}`;
}
}
return msg;
}
}
const ErrPrototype = {
__proto__: null!,
toString: function (this: Err<any>): string {
return String(this.error);
},
[Symbol.for("nodejs.util.inspect.custom")](): string {
return this.toString();
},
get [SymbolToStringTag](): string {
return "ValidationError";
},
} as unknown as Err<any> & ThisType<Err<any>>;
export type ValidationResult<T = any, E = string | Error> = Ok<T> | Err<E>;
type ErrorLike<T> =
| string
| { error: unknown; path: PropertyKey[] }
| ValidationError<T>;
export class Context<T = any, I = any, O = T> {
#errors: ErrorLike<T>[] = [];
constructor(
readonly path: PropertyKey[] = [],
readonly type: Type<T> | null = null,
) {}
get errors(): ValidationError<T>[] {
const { path, type } = this;
return this.#errors.map((e) => ValidationError.from<T>(e, { path, type }));
}
add(...errors: ErrorLike<T>[]): this {
this.#errors.push(...errors);
this.#errors = this.#errors.filter((e, i, a) =>
e != null && a.indexOf(e) === i
);
return this;
}
ok(value: T): Ok<T> {
return { __proto__: null!, success: true, value } as Ok<T>;
}
/**
* Creates a contextualized {@linkcode ValidationResult} error with a given
* message and optional subpath. If provided, the path will be appended to
* the error message in dot notation, relative to the current context path.
*
* @param message - The error message or an `Error` object.
* @param [path] - An optional subpath to append to the error message.
* @returns A new {@linkcode ValidationResult} object with `success: false`.
*/
err<E extends string | Error>(
message: E,
path?: PropertyKey[],
): Err<E>;
/**
* Creates a contextualized {@linkcode ValidationResult} error from the
* provided template string literal and values. The interpolated template
* values are each appended to the template after being rendered with the
* `inspect` function from the `node:util` module.
*
* @param template - The template string literal.
* @param values - The values to interpolate into the template.
* @returns A new {@linkcode ValidationResult} object with `success: false`.
* @example
* ```ts
* import * as t from "@nick/lint-core/runtime-types";
*
* class MyType<T> extends t.Type<T> {
* constructor(override name: string, readonly value: T) {
* super(name, { value, validator: (x): x is T => x === value });
* }
*
* validate(v: unknown, ctx: t.Context): t.ValidationResult<T> {
* if (!this.is(v)) {
* return ctx.err`Expected ${this}, got {v} ({typeof v})`;
* }
* return ctx.ok(v);
* }
* }
*
* const six = new MyType("six", 6);
* const result = six.decode(5);
*
* console.log(result.error);
* // Expected [MyType<six>], got 5 (type: 'number')
* ```
*/
err<E = any>(
template: TemplateStringsArray,
...values: unknown[]
): Err<E>;
/** @internal */
err<E>(
message: TemplateStringsArray | string | Error,
...paths: unknown[]
): Err<E> {
let path: PropertyKey[] = [];
if (isTemplateStringsArray(message)) {
const options = { ...defaultInspectOptions };
let str = "";
for (let i = 0; i < message.length; i++) {
str += message[i];
if (i < path.length) str += Type.inspect(path[i], options);
}
message = str;
path.length = 0;
}
path = [...this.path, ...path];
if (!path.length) path.push("<root>");
let error = message.toString();
if (isError(message)) {
const e = message;
error = `${e.message || message}`;
const path = [...this.path];
if ("path" in e && isArray(e.path, isPropertyKey)) {
path.push(...e.path);
}
if (path.length > 0) error += `\n at ${path.join(".")}`;
if (e.cause) {
error += `\nCaused by:\n${String(e.cause).replace(/^/mg, " ╎ ")}`;
}
} else if (this.path.length > 0) {
error += `\n at ${this.path.join(".")}`;
}
const errors = [...this.errors];
return new ValidationError(error, {
path,
type: this.type,
errors,
cause: message,
});
}
/**
* Creates a new context from the provided path, such that the new instance
* is relative to the path of the current instance (i.e., a child context).
*
* The subpath can be a single string or an array of strings. If an array is
* provided, it will be flattened and concatenated with the parent path.
*
* @param path - The path to append to the current context.
* @returns A new {@linkcode Context} object with the merged path.
* @example
* ```ts
*
* ```
*/
with(...path: PropertyKey[] | [PropertyKey[]]): Context<T, I, O> {
return new Context([...this.path, ...path].flat());
}
}
const identity = <T>(x: T): T => x;
export type TypeConstraint<
U extends T,
T = unknown,
K extends PropertyKey = PropertyKey,
> = {
readonly name: K;
predicate(value: T): value is U;
message?: string;
} | {
readonly name: K;
predicate(value: T): unknown;
message?: string;
};
// ----------------------------------------------------------------------------
// TypeInfo
// ----------------------------------------------------------------------------
export interface TypeInfoState<V = unknown, T extends Type<V> = Type<V>> {
name: string;
type: T;
args: ConstructorParameters<T["constructor"]>;
default: V;
optional: boolean;
nullable: boolean;
description: string;
constraints: TypeConstraint<V>[];
validator(input: unknown): input is V;
[key: PropertyKey]: any;
}
const defaultTypeInfoState = {
name: "any",
type: undefined! as Any,
args: [] as any[],
default: undefined as any,
optional: false,
nullable: false,
description: "Any type",
constraints: [],
validator: (input): input is any => (void input, true),
} as const satisfies TypeInfoState;
export type DefaultTypeInfoState = typeof defaultTypeInfoState;
type WithTypeInfo<
V,
T extends Type<V>,
S extends TypeInfoState<V, T | Type<V>>,
I extends Partial<TypeInfoState<V>>,
> = TypeInfo<
V,
T,
TypeInfoState<V, T> & Pick<S, Exclude<keyof S, keyof I>> & I
> extends infer V ? V : never;
/**
* The `TypeInfo` class is used internally to store metadata and additional type
* information on instances of {@linkcode Type} subclasses. It provides support
* for managing extra type constraints, custom error messages, human-readable
* names/descriptions, and various other parts of the different states of a
* given Type instance.
*
* @internal
*/
export class TypeInfo<
V,
T extends Type<V> = Type<V>,
S extends TypeInfoState<V, T> = TypeInfoState<V, T>,
> {
constructor(
readonly _: S = { ...defaultTypeInfoState } as DefaultTypeInfoState & S,
) {}
get state(): S {
return this._;
}
name<K extends string>(
typeName: K,
): WithTypeInfo<V, T, S, { name: K }> {
this._.name = typeName;
return this as never;
}
type<U extends Type<any> = Type<V>>(
type: U,
): WithTypeInfo<V, T & U, S, { type: U }> {
(this as TypeInfo<any>)._.type = type;
return this as never;
}
default<K extends V>(value: K): WithTypeInfo<V, T, S, { default: K }> {
this._.default = value;
return this as never;
}
optional<O extends boolean = true>(
optional: O = true as O,
): WithTypeInfo<V, T, S, { optional: O }> {
this._.optional = !!optional;
return this as never;
}
nullable<N extends boolean = true>(
nullable: N = true as N,
): WithTypeInfo<V, T, S, { nullable: N }> {
this._.nullable = !!nullable;
return this as never;
}
description<K extends string>(
description: K,
): WithTypeInfo<V, T, S, { description: K }> {
this._.description = description;
return this as never;
}
validator<K extends (input: unknown) => input is V>(
validator: K,
): WithTypeInfo<V, T, S, { validator: K }> {
this._.validator = validator;
return this as never;
}
constraint<K extends PropertyKey, U extends V>(
name: K,
predicate: (value: V) => value is U,
message?: string,
): WithTypeInfo<
V,
T,
S,
{ constraints: [...S["constraints"], TypeConstraint<U, V, K>] }
>;
constraint<K extends PropertyKey>(
name: K,
predicate: (value: V) => unknown,
message?: string,
): WithTypeInfo<
V,
T,
S,
{ constraints: [...S["constraints"], TypeConstraint<V, V, K>] }
>;
constraint<K extends PropertyKey>(
name: K,
predicate: (value: V) => unknown,
message?: string,
): WithTypeInfo<
V,
T,
S,
{ constraints: [...S["constraints"], TypeConstraint<V, V, K>] }
> {
return this.constraints([...this._.constraints, {
name,
predicate,
message,
}]) as never;
}
constraints<K extends TypeConstraint<V>[]>(
constraints: K,
): WithTypeInfo<V, T, S, { constraints: K }> {
this._.constraints = constraints;
return this as never;
}
getName(): S["name"] {
return this._.name;
}
getType(): S["type"] {
return this._.type;
}
getDefault(): S["default"] {
return this._.default;
}
getOptional(): S["optional"] {
return this._.optional;
}
getNullable(): S["nullable"] {
return this._.nullable;
}
getDescription(): S["description"] {
return this._.description;
}
getValidator(): S["validator"] {
return this._.validator;
}
getConstraints(): S["constraints"] {
return this._.constraints;
}
hasName(): this is WithTypeInfo<V, T, S, { name: NonNullable<S["name"]> }>;
hasName<K extends string>(
name: K,
): this is WithTypeInfo<V, T, S, { name: K }>;
hasName(name?: string) {
return this._.name != null && (!name || this._.name === name);
}
hasType(): this is WithTypeInfo<V, T, S, { type: NonNullable<S["type"]> }>;
hasType<K extends Type<V>>(
type: K,
): this is WithTypeInfo<V, T, S, { type: K }>;
hasType(type?: Type<V>) {
return this._.type != null && (!type || this._.type === type);
}
hasDefault(): this is WithTypeInfo<
V,
T,
S,
{ default: NonNullable<S["default"]> }
>;
hasDefault<K extends V>(
value: K,
): this is WithTypeInfo<V, T, S, { default: K }>;
hasDefault(value?: V) {
return this._.default != null && (!value || this._.default === value);
}
hasOptional(): this is WithTypeInfo<
V,
T,
S,
{ optional: NonNullable<S["optional"]> }
>;
hasOptional<K extends boolean>(
optional: K,
): this is WithTypeInfo<V, T, S, { optional: K }>;
hasOptional(optional?: boolean) {
return this._.optional != null &&
(!optional || this._.optional === optional);
}
hasNullable(): this is WithTypeInfo<
V,
T,
S,
{ nullable: NonNullable<S["nullable"]> }
>;
hasNullable<K extends boolean>(
nullable: K,
): this is WithTypeInfo<V, T, S, { nullable: K }>;
hasNullable(nullable?: boolean) {
return this._.nullable != null &&
(!nullable || this._.nullable === nullable);
}
hasDescription(): this is WithTypeInfo<
V,
T,
S,
{ description: NonNullable<S["description"]> }
>;
hasDescription<K extends string>(
description: K,
): this is WithTypeInfo<V, T, S, { description: K }>;
hasDescription(description?: string) {
return this._.description != null &&
(!description || this._.description === description);
}
hasValidator(): this is WithTypeInfo<
V,
T,
S,
{ validator: NonNullable<S["validator"]> }
>;
hasValidator<K extends (input: unknown) => input is V>(
validator: K,
): this is WithTypeInfo<V, T, S, { validator: K }>;
hasValidator(validator?: (input: unknown) => input is V) {
return this._.validator != null &&
(!validator || this._.validator === validator);
}
hasConstraints(): this is WithTypeInfo<
V,
T,
S,
{ constraints: NonNullable<S["constraints"]> }
>;
hasConstraints<K extends S["constraints"]>(
constraints: K,
): this is WithTypeInfo<V, T, S, { constraints: K }>;
hasConstraints(constraints?: S["constraints"]) {
return this._.constraints != null &&
(!constraints || this._.constraints === constraints);
}
}
const _type: unique symbol = Symbol("type");
type _type = typeof _type;
const _spec: unique symbol = Symbol("spec");
type _spec = typeof _spec;
function deepClone<const T>(arg: T): T;
function deepClone(arg: any): any {
if (!arg || isPrimitive(arg)) return arg;
if (arg instanceof Type) return arg.clone();
if (isArrayBuffer(arg)) return arg.slice(0);
if (isTypedArray(arg)) return arg.slice(0);
if (isDataView(arg)) {
const { buffer, byteLength, byteOffset } = arg;
return new DataView(buffer.slice(0), byteOffset, byteLength);
}
if (isArray(arg)) return arg.map(deepClone);
if (isPlainObject(arg)) return { ...arg };
if (isSet(arg)) return new Set([...arg].map(deepClone));
if (isMap(arg)) {
return new Map([...arg].map(([k, v]) => deepClone([k, v])));
}
if (isWeakSet(arg)) return new WeakSet();
if (isWeakMap(arg)) return new WeakMap();
if (isWeakRef(arg) && arg.deref()) return new WeakRef(arg.deref()!);
if (isError(arg)) {
return new (
arg.constructor as typeof Error
)(arg.message, { cause: arg.cause });
}
if (isDate(arg)) return new Date(arg.getTime());
if (isRegExp(arg)) return new RegExp(arg.source, arg.flags);
return arg;
}
// ----------------------------------------------------------------------------
// Core Type class
// ----------------------------------------------------------------------------
export interface Type<T = unknown, I = any, O = T> {
(...args: ConstructorParameters<this["constructor"]>): this;
readonly constructor: new (...args: any[]) => Type<T>;
}
export abstract class Type<T = unknown, I = any, O = T>
extends Callable<void, unknown[], Type<T, I, O>> {
#info: TypeInfo<T>;
#ctx: Context<T, I, O> | undefined;
declare readonly _typeof: T;
constructor(
readonly name: string = "Type",
info?: Partial<TypeInfoState<T>>,
) {
super((...args) =>
this.with(...args as ConstructorParameters<this["constructor"]>)
);
const args = [name, info];
if (new.target !== Type) {
args.shift(); // drop the name for subclasses by default
}
this.#info = new TypeInfo({
...defaultTypeInfoState,
args,
type: this,
name,
...info,
});
}
get context(): Context<T, I, O> {
return this.#ctx ??= new Context();
}
get info(): TypeInfo<T, this> {
return this.#info as TypeInfo<T, this>;
}
is(input: unknown): input is T {
const fn = this.info.getValidator();
let ok = false;
if (isFunction(fn)) ok = FunctionPrototypeCall(fn, this, input);
if (ok) {
const constraints = this.info.getConstraints();
for (const { predicate } of constraints) {
if (!FunctionPrototypeCall(predicate, this, input)) return false;
}
return true;
}
return false;
}
validate(input: unknown, ctx: Context<T, I, O>): ValidationResult<T> {
ctx ??= this.context;
const fn = this.info.getValidator();
let ok = false;
if (isFunction(fn)) ok = FunctionPrototypeCall(fn, this, input);
if (ok) {
const constraints = this.info.getConstraints();
for (const { name, predicate, message } of constraints) {
if (!FunctionPrototypeCall(predicate, this, input)) {
let msg = message ??
`Input failed to satisfy the '${name.toString()}' constraint on type ${this}.`;
msg = msg.replace(
/%(\w+)|\{\{\s*(\w+)\s*\}\}|\{\s*(\w+)\s*\}/g,
(m, a, b, c) => {
const key = a ?? b ?? c;
let val = ReflectGet(this, key) ?? ReflectGet(this.info._, key);
if (key === "type") {
val = this.info.getType();
} else if (key === "name") {
val = this.info.getName();
} else if (
key === "input" || key === "value" || key === "actual" ||
key === "s" || key === "i" || key === "o" || key === "v"
) {
val = input;
} else if (key === "path") {
val = ctx.path.length ? ctx.path.join(".") : "<root>";
} else if (key === "errors" && ctx.errors.length) {
val = " - " + ctx.errors.map((e) => e.error).join(" - ");
}
if (val == null) return m;
return String(val);
},
);
return ctx.err(msg);
}
}
return ctx.ok(input as T);
}
return ctx.err`Type '${input}' is not assignable to type '${this}'`;
}
as<U>(input: U): Extract<U, T>;
as(input: unknown): T;
as(input: unknown): T {
this.assert(input);
return input;
}
assert(input: unknown): asserts input is T {
const res = this.decode(input);
if (!res.success) {
throw new ValidationError(res.error, { path: res.path, type: this });
}
}
decode(input: unknown): ValidationResult<T> {
return this.validate(input, this.context);
}
or<B, O extends Type<B>>(other: O | Type<B>): UnionType<T, B> {
if (!(other instanceof Type)) {
throw new TypeError(
`Expected 'other' to be a Type instance, but received ${typeof other}: ${other}`,
);
}
return new UnionType(this, other);
}
and<B, O extends Type<B>>(other: O | Type<B>): IntersectionType<T, B> {
if (!(other instanceof Type)) {
throw new TypeError(
`Expected 'other' to be a Type instance, but received ${typeof other}: ${other}`,
);
}
return new IntersectionType(this, other);
}
not<B, O extends Type<B> = Type<B>>(other: O | Type<B>): ExcludeType<T, B> {
if (!(other instanceof Type)) {
throw new TypeError(
`Expected 'other' to be a Type instance, but received ${typeof other}: ${other}`,
);
}
return new ExcludeType(this, other);
}
xor<B, O extends Type<B> = Type<B>>(other: O | Type<B>): ExcludeType<T, B> {
if (!(other instanceof Type)) {
throw new TypeError(
`Expected 'other' to be a Type instance, but received ${typeof other}: ${other}`,
);
}
return new ExcludeType(this, other);
}
optional(): OptionalType<T> {
return new OptionalType(this);
}
nullable(): NullableType<T> {
return new NullableType(this);
}
notnull(): NonNullableType<T> {
return new NonNullableType(this);
}
map<U>(mapper: (input: T) => U): Type<U> {
if (typeof mapper !== "function") {
throw new TypeError(
`Expected a mapper function, but received ${typeof mapper}`,
);
}
return new MappedType(this, mapper);
}
flatMap<U>(mapper: (input: T) => U | Type<U>): Type<U> {
if (typeof mapper !== "function") {
throw new TypeError(
`Expected a mapper function, but received ${typeof mapper}`,
);
}
return new MappedType(this, (input) => {
const t = mapper.call(this, input);
return t != null && t instanceof Type ? t.as(input) : t;
});
}
filter(predicate: (input: T) => boolean): Type<T> {
if (typeof predicate !== "function") {
throw new TypeError(
`Expected a predicate function, but received ${typeof predicate}`,
);
}
return new FilterType(this, predicate);
}
transmute<U>(transmuter: (input: T) => U): Type<U> {
if (typeof transmuter !== "function") {
throw new TypeError(
`Expected a transmuter function, but received ${typeof transmuter}`,
);
}
return new TransmuteType(this, transmuter);
}
cast<U>(caster: (input: T) => U): Type<U> {
if (typeof caster !== "function") {
throw new TypeError(
`Expected a caster function, but received ${typeof caster}`,
);
}
return new CastType(this, caster);
}
refine<U extends T>(
refiner: (it: T) => it is U,
message?: string,
): Type<U>;
refine<U extends T>(
refiner: (it: T) => unknown,
message?: string,
): Type<U>;
refine<U extends T>(
refiner: (it: T) => it is U,
message?: string,
): Type<U> {
if (typeof refiner !== "function") {
throw new TypeError(
`Expected a refiner function, but received ${typeof refiner}`,
);
}
const o = { [_type]: this, type: this };
const p = { ...o };
return RefineType.from(this, refiner, { message });
}
brand<U, K extends PropertyKey>(
key: K,
value: U,
): Type<T & { [P in K]: U }> {
return new BrandType(this, key, value);
}
clone(): this {
const info = {
...this.info.state,
type: this,
constraints: [...this.info.getConstraints()].map((c) => ({ ...c })),
};
let args = [...this.#info.state.args];
if (args.length) args = deepClone(args);
const clone = new (this.constructor as any)(...args);
info.args = args as any;
clone.#info._ = info;
return clone;
}
addConstraint<K extends PropertyKey, V = any>(
predicate: (value: T) => boolean,
key: K,
message = `Expected ${this.name} to satisfy ${key.toString()}`,
): this & Type<T & { [P in K]: V }> {
this.info.constraint(key, predicate, message);
return this as never;
}
withConstraint<K extends PropertyKey, V = any>(
predicate: (value: T) => boolean,
key: K,
message?: string,
): this & Type<T & { [P in K]: V }> {
return this.clone().addConstraint(predicate, key, message);
}
/**
* Creates a new instance of this type using the provided arguments. The
* constructor of the type is called with the provided arguments, and the
* resulting instance is returned.
*
* This is useful for refining certain types with additional configurations,
* especially when used on the built-in instances provided by this library
* like `string`, `number`, `boolean`, etc.
*
* @example
* ```ts
* import * as t from "@nick/lint-core/runtime-types";
*
* const str = t.string.with({ minLength: 5 });
* ```
* @example
* ```ts
* import * as t from "@nick/lint-core/runtime-types";
*
* const map1 = t.map.with(t.string, t.unknown);
*
* // the following is equivalent to the above call, for convenience:
* const map2 = t.map(t.string, t.unknown);
* ```
*/
with<U extends T>(
...args: ConstructorParameters<this["constructor"]>
): Type<U> {
const Ctor = this.constructor as any as new (...a: typeof args) => Type<U>;
return new Ctor(...args);
}
[inspect.custom](depth: number | null, options: InspectOptionsStylized) {
const s = options?.stylize ?? stylize;
let type = this.toString();
// if (this.info.hasType()) type = this.info.getType().toString();
if (depth == null || depth < 1) type = `[${type}]`;
if (!getNoColor()) type = s(type, "special");
return type;
}
toJSON(): unknown {
return this.name;
}
override toString(): string {
return this.name;
}
/**
* Utility for rendering an arbitrary value in a string format suitable for
* text-only contexts like a terminal emulator or browser console. This is
* a wrapper around the `inspect` function from `node:util`, with some custom
* defaults that align better with the specific needs of our use case.
*
* @param value The value to inspect.
* @param [options] An optional options object for customizing the output of
* the inspect process. By default, colors are disabled, objects with 3 props
* or less are printed in compact mode, getters are rendered, and
*/
static inspect(value: unknown, options?: InspectOptionsStylized): string {
return inspect(value, {
colors: false,
compact: 3,
getters: "get",
customInspect: true,
sorted: true,
depth: 8,
...options,
});
}
static [inspect.custom](
_depth: number | null,
options: InspectOptionsStylized,
): string {
if (!Object.prototype.isPrototypeOf.call(this, Type)) return this.name;
let s = options?.stylize ?? stylize;
if (getNoColor()) s = identity;
return s(
`[Type${this.name === "Type" ? "" : `<${this.name}>`}]`,
"special",
);
}
static {
Object.assign(this, { defaultRenderOptions, defaultInspectOptions });
}
}
export declare namespace Type {
export type of<T, F = never> = Type.type<T, F>;
export type type<T, F = never> = T extends Type<infer U, any, any> ? U : F;
export type input<T, F = never> = T extends Type<any, infer I, any> ? I : F;
export type output<T, F = never> = T extends Type<any, any, infer O> ? O : F;
}
export interface AnyType extends Type<any, any, any> {}
export interface UnknownType extends Type<unknown, any, unknown> {}
// ----------------------------------------------------------------------------
// Primitive Types
// ----------------------------------------------------------------------------
type PrimitiveTypeMap = {
string: string;
number: number;
boolean: boolean;
bigint: bigint;
symbol: symbol;
undefined: undefined;
null: null;
};
export type Primitive =
| string
| number
| bigint
| boolean
| symbol
| null
| undefined
| void;
export class PrimitiveType<T extends Primitive = Primitive> extends Type<T> {
static override readonly name: "Primitive" | strings = "Primitive";
override name: "Primitive" | strings;
constructor(name: string, info?: Partial<TypeInfoState<T>>);
constructor(info?: Partial<TypeInfoState<T>>);
constructor(...args: unknown[]);
constructor(
name?: string | Partial<TypeInfoState<T>>,
info?: Partial<TypeInfoState<T>>,
) {
name ??= PrimitiveType.name;
if (!isString(name)) {
info = name;
name = String(info.name ?? PrimitiveType.name);
}
const validator = (input: unknown): input is T => {
if (this.name === PrimitiveType.name) {
return isPrimitive(input);
} else if (this.name === "null") {
return isNull(input);
} else {
// deno-lint-ignore valid-typeof
return this.name === typeof input;
}
};
info ??= { name, validator };
if (new.target === PrimitiveType) {
info.validator = validator;
info.name = name = PrimitiveType.name;
info.args = [name, info];
} else {
info.args = [info];
}
super(name as string, info);
this.name = name as string;
}
validator(input: unknown): input is T {
const fn = this.info.getValidator();
if (!fn(input)) return false;
const constraints = this.info.getConstraints();
return [...constraints].every(({ predicate }) => predicate(input));
}
override as(input: unknown): T {
if (this.is(input)) return input;
throw new TypeError(
`Expected ${this.name}, got ${input === null ? "null" : typeof input}`,
);
}
override validate(input: unknown, ctx: Context<T>): ValidationResult<T> {
if (this.is(input)) return ctx.ok(input);
for (const { name, predicate, message } of this.info.getConstraints()) {
if (!predicate(input)) {
ctx.add(
message ??
`Input failed to satisfy the '${name.toString()}' constraint.`,
);
}
}
return ctx.err`Type ${input} is not assignable to type ${this}`;
}
}
export const primitive: PrimitiveType<Primitive> = new PrimitiveType();
const _string_length_gt: unique symbol = Symbol(
"string minimum length (exclusive)",
);
const _string_length_lt: unique symbol = Symbol(
"string maximum length (exclusive)",
);
const _string_length_gte: unique symbol = Symbol(
"string minimum length (inclusive)",
);
const _string_length_lte: unique symbol = Symbol(
"string maximum length (inclusive)",
);
const _string_length_eq: unique symbol = Symbol("string length equals");
const _string_matches: unique symbol = Symbol("string matches");
const _string_matches_strict: unique symbol = Symbol("string matches strict");
const _string_not_matches: unique symbol = Symbol("string does not match");
const _string_is_email: unique symbol = Symbol("string is email");
const _string_is_url: unique symbol = Symbol("string is url");
const _string_is_uuid: unique symbol = Symbol("string is uuid");
const _string_is_date: unique symbol = Symbol("string is date");
const _string_is_hex: unique symbol = Symbol("string is hex");
const _string_is_base64: unique symbol = Symbol("string is base64");
const _string_is_json: unique symbol = Symbol("string is json");
const _string_is_utf8: unique symbol = Symbol("string is utf8");
export interface StringTypeInfoState<T extends string = string>
extends TypeInfoState<T, StringType<T>> {
constraints: TypeConstraint<T>[];
}
export interface StringType<T extends string = string> {
readonly constructor: typeof StringType<T>;
}
export class StringType<T extends string = string> extends PrimitiveType<T> {
static override readonly name = "string";
constructor(info?: Partial<StringTypeInfoState<T>>) {
super("string", info);
}
/**
* Validates a string against a minimum length constraint (inclusive).
*
* @param min The minimum length.
* @param inclusive Whether or not the length constraint should be inclusive.
* If `true`, the comparison is performed with the `>=` operator; otherwise,
* it will be performed using the `>` operator. Defaults to `false`.
* @returns A copy of this `StringType` instance with an additional constraint
* requiring the string length to be greater than or equal to `min`.
*/
min<N extends number>(
min: N,
inclusive: true,
): StringType<T & { "len >=": N }>;
/**
* Validates a string against a minimum length constraint (exclusive).
*
* @param min The minimum length.
* @param [inclusive=false] Controls the exclusivity of the constraint. If
* `true`, the comparison is performed with the `>=` operator; otherwise, it
* will be performed with the `>` operator. Defaults to `false`.
*/
min<N extends number>(
min: N,
inclusive?: false,
): StringType<T & { "len >": N }>;
/** @internal */
min<N extends number>(
min: N,
inclusive: boolean,
): StringType<T & ({ "len >": N } | { "len >=": N })>;
/** @internal */
min(min: number, inc?: boolean): AnyType {
return this.withConstraint(
(s) => (inc && s.length === min) || s.length > min,
`len ${inc ? ">=" : ">"}`,
`Expected string length to be ${inc ? ">=" : ">"} ${min}`,
);
}
/**
* Adds a constraint to the string type requiring its length be less than
* or equal to the provided `max` value. If `inclusive` is `true`, the check
* uses the inclusive `<=` operator; otherwise, the exclusive `<` is used to
* perform the comparison.
*
* @param max The maximum length.
* @param inclusive Controls the inclusivity of the constraint.
* @returns A new {@linkcode StringType} with the maximum length constraint.
*/
max<N extends number>(
max: N,
inclusive: true,
): StringType<T & { "len <=": N }>;
/**
* Adds a constraint to the string type requiring its length be less than
* or equal to the provided `max` value. If `inclusive` is `true` the check
* uses the inclusive `<=` operator; otherwise, the exclusive `<` is used to
* perform the comparison.
*
* @param max The maximum length.
* @param [inclusive=false] Controls the exclusivity of the constraint.
* @returns A new {@linkcode StringType} with the maximum length constraint.
*/
max<N extends number>(
max: N,
inclusive?: false,
): StringType<T & { "len <": N }>;
/** @internal */
max<N extends number>(
max: N,
inclusive: boolean,
): StringType<T & ({ "len <": N } | { "len <=": N })>;
/** @internal */
max(max: number, inc?: boolean): AnyType {
return this.withConstraint(
(s) => (inc && s.length === max) || s.length < max,
`len ${inc ? "<=" : "<"}`,
`Expected string length to be ${inc ? "<=" : "<"} ${max}`,
);
}
/**
* Validates a string with a specific length.
*
* @param length The exact length.
* @returns A new {@linkcode StringType} with the exact length constraint.
*/
len<N extends number>(
length: N,
): StringType<T & { "len =": N }>;
/**
* Validates a string with a specific length.
*
* @param length The exact length.
* @returns A new {@linkcode StringType} with the exact length constraint.
*/
len(length: number): StringType<T & { "len =": number }>;
/** @internal */
len(length: number): AnyType {
return this.withConstraint(
(s) => s.length === length,
"len =",
`Expected string length to be ${length}`,
);
}
/**
* Validates a string against a specific RegExp pattern or string.
*
* @param pattern The regular expression pattern, or a string to
* create a new RegExp instance from.
* @returns A new {@linkcode StringType} with a pattern constraint, which
* further validates strings, ensuring they satisfy the constraint's pattern.
*/
match<P extends string | RegExp>(
pattern: P,
message?: string,
): StringType<
T & {
" matches$": P;
}
>;
/**
* Validates a string with a specific pattern.
*
* @param pattern The regular expression pattern.
* @returns A new {@linkcode StringType} with the pattern constraint.
*/
match(pattern: string | RegExp, message?: string): StringType<
T & {
" matches$": string | RegExp;
}
>;
/** @internal */
match(pattern: string | RegExp, message?: string): AnyType {
return this.withConstraint(
// we construct a new RegExp instance on each call to avoid the footgun
// issues surrounding the lastIndex property. otherwise we would have to
// make sure we reset pattern.lastIndex = 0 before every .test call.
(s): s is T => new RegExp(pattern).test(s),
"matches pattern",
message ?? `Expected the string to match the pattern: ${pattern}`,
);
}
/**
* Validates a string that is a valid email address.
*
* @returns A new {@linkcode StringType} with the email constraint.
*/
email(negate: true): StringType<T & { "is email": false }>;
email(negate: false): StringType<T & { "is email": true }>;
email(negate?: boolean): StringType<T & { "is email": boolean }>;
/** @internal */
email(negate?: boolean): AnyType {
const pat = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
return this.withConstraint(
(s) => {
pat.lastIndex = 0;
return pat.test(s) === !negate;
},
"is email",
"Expected a valid email address (e.g. [email protected])",
);
}
/**
* Validates a string that is a valid URL.
*
* @returns A new {@linkcode StringType} with the URL constraint.
*/
url(): StringType<T & { "is url": true }>;
url(negate: true): StringType<T & { "is url": false }>;
url(negate?: boolean): StringType<T & { "is url": boolean }>;
/** @internal */
url(negate?: boolean): AnyType {
return this.withConstraint(
(s): s is any => isURLString(s) === !negate,
" is url",
"Expected a valid URL string (e.g. https://github.com/nberlette)",
);
}
/**
* Validates a string that is a valid UUID.
*
* @returns A new {@linkcode StringType} with the UUID constraint.
*/
uuid(): StringType<T & { "is uuid": true }>;
uuid(negate: true): StringType<T & { "is uuid": false }>;
uuid(negate?: boolean): StringType<T & { "is uuid": boolean }>;
/** @internal */
uuid(negate?: boolean): AnyType {
return this.withConstraint(
(s): s is any =>
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
s,
) === !negate,
"is uuid",
"Expected a valid UUID string (e.g. 550e8400-e29b-41d4-a716-446655440000)",
);
}
/**
* Validates a string that is a valid ISO date.
*
* @returns A new {@linkcode StringType} with the ISO date constraint.
*/
date(): StringType<T & { "is date": true }>;
date(negate: true): StringType<T & { "is date": false }>;
date(negate?: boolean): StringType<T & { "is date": boolean }>;
/** @internal */
date(negate?: boolean): AnyType {
return this.withConstraint(
(s) => isDateString(s) === !negate,
"is date",
"Expected a valid date string (e.g. 2021-01-06)",
);
}
override refine<U extends T>(
predicate: (value: T) => value is U,
message?: string,
): StringType<U>;
override refine(
predicate: (value: T) => boolean,
message?: string,
): StringType<T>;
override refine<U extends T>(
predicate: (value: T) => value is U,
message?: string,
): StringType<U> {
return ObjectSetPrototypeOf(
super.refine(predicate, message),
StringType.prototype,
);
}
}
export const string: StringType = new StringType();
export interface NumberType<N extends number = number> {
readonly constructor: typeof NumberType<N>;
}
export class NumberType<N extends number = number> extends PrimitiveType<N> {
static override readonly name = "number";
constructor(info?: Partial<TypeInfoState<N, NumberType<N>>>) {
super("number", info);
}
/**
* Validates a number against a minimum value constraint (inclusive).
*
* @param min The minimum value.
* @param inclusive Whether or not the value constraint should be inclusive.
* If `true`, the comparison is performed with the `>=` operator; otherwise,
* it will be performed using the `>` operator. Defaults to `false`.
*/
min<M extends number>(
min: M,
inclusive: true,
): NumberType<N & { ">=": M }>;
/**
* Validates a number against a minimum value constraint (exclusive).
*
* @param min The minimum value.
* @param [inclusive=false] Controls the exclusivity of the constraint. If
* `true`, the comparison is performed with the `>=` operator; otherwise, it
* will be performed using the `>` operator. Defaults to `false`.
*/
min<M extends number>(
min: M,
inclusive?: false,
): NumberType<N & { ">": M }>;
/** @internal */
min<M extends number>(
min: M,
inclusive: boolean,
): NumberType<N & ({ ">": M } | { ">=": M })>;
/** @internal */
min(min: number, inc?: boolean): AnyType {
return this.withConstraint(
(n) => (inc && n >= min) || n > min,
inc ? ">=" : ">",
`Expected number to be ${inc ? ">=" : ">"} ${min}`,
);
}
/**
* Validates a number against a maximum value constraint (inclusive).
*
* @param max The maximum value.
* @param inclusive Whether or not the value constraint should be inclusive.
* If `true`, the comparison is performed with the `<=` operator; otherwise,
* it will be performed using the `<` operator. Defaults to `false`.
*/
max<M extends number>(
max: M,
inclusive: true,
): NumberType<N & { "<=": M }>;
/**
* Validates a number against a maximum value constraint (exclusive).
*
* @param max The maximum value.
* @param [inclusive=false] Controls the exclusivity of the constraint. If
* `true`, the comparison is performed with the `<=` operator; otherwise, it
* will be performed using the `<` operator. Defaults to `false`.
*/
max<M extends number>(
max: M,
inclusive?: false,
): NumberType<N & { "<": M }>;
/** @internal */
max<M extends number>(
max: M,
inclusive: boolean,
): NumberType<N & ({ "<": M } | { "<=": M })>;
/** @internal */
max(max: number, inc?: boolean): AnyType {
return this.withConstraint(
(n) => (inc && n <= max) || n < max,
inc ? "<=" : "<",
`Expected number to be ${inc ? "<=" : "<"} ${max}`,
);
}
/**
* Creates a new `NumberType` from the current instance, adding a constraint
* that ensures its value is equal to the provided `value`.
*
* @param value The exact value.
* @returns A new {@linkcode NumberType} with the exact value constraint.
*/
eq<M extends number>(
value: M,
): NumberType<M & { "===": M }>;
/**
* Creates a new `NumberType` from the current instance, adding a constraint
* that ensures its value is equal to the provided `value`.
*
* @param value The exact value.
* @returns A new {@linkcode NumberType} with the exact value constraint.
*/
eq(value: number): NumberType<N & { "===": number }>;
/** @internal */
eq(value: number): AnyType {
return this.withConstraint(
(n) => n === value,
"===",
`Expected number to be ${value}`,
);
}
/**
* Creates a new `NumberType` from the current instance, adding a constraint
* that ensures its value is a valid integer.
*
* @returns A new {@linkcode NumberType} with the integer constraint.
*/
integer(): NumberType<N & { "is integer": true }>;
/** @internal */
integer(): AnyType {
return this.withConstraint(
(n) => isInteger(n),
"is integer",
"Expected number to be an integer",
);
}
/**
* Creates a new `NumberType` from the current instance, adding a constraint
* that ensures its value is a valid floating point number (float).
*
* @returns A new {@linkcode NumberType} with the float constraint.
*/
float(): NumberType<N & { "is float": true }>;
/** @internal */
float(): AnyType {
return this.withConstraint(
(n) => isFloat(n),
"is float",
"Expected number to be a float",
);
}
/**
* Creates a new `NumberType` from the current instance, adding a constraint
* that ensures its value is a valid finite number.
*
* @returns A new {@linkcode NumberType} with the finite constraint.
*/
finite(): NumberType<N & { "is finite": true }>;
/** @internal */
finite(): AnyType {
return this.withConstraint(
(n) => isFinite(n),
"is finite",
"Expected number to be finite",
);
}
/**
* Creates a new `NumberType` from the current instance, adding a constraint
* that ensures its value is a valid safe integer.
*
* @returns A new {@linkcode NumberType} with the safe integer constraint.
*/
safeInteger(): NumberType<N & { "is safe-integer": true }>;
/** @internal */
safeInteger(): AnyType {
return this.withConstraint(
(n) => Number.isSafeInteger(n),
"is safe-integer",
"Expected number to be a safe integer",
);
}
/**
* Creates a new `NumberType` from the current instance, adding a constraint
* that ensures its value is a valid positive number.
*
* @returns A new {@linkcode NumberType} with the positive constraint.
*/
positive(): NumberType<N & { "is positive": true }>;
/** @internal */
positive(): AnyType {
return this.withConstraint(
(n) => isPositive(n),
"is positive",
"Expected number to be positive",
);
}
/**
* Creates a new `NumberType` from the current instance, adding a constraint
* that ensures its value is a valid negative number.
*
* @returns A new {@linkcode NumberType} with the negative constraint.
*/
negative(): NumberType<N & { "is negative": true }>;
/** @internal */
negative(): AnyType {
return this.withConstraint(
(n) => isNegative(n),
"is negative",
"Expected number to be negative",
);
}
/**
* Creates a new `NumberType` from the current instance, adding a constraint
* that ensures its value is a valid non-zero number.
*
* @returns A new {@linkcode NumberType} with the non-zero constraint.
*/
nonzero(): NumberType<N & { "not zero": true }>;
/** @internal */
nonzero(): AnyType {
return this.withConstraint(
(n) => isNonZero(n),
"not zero",
"Expected a non-zero number",
);
}
}
export const number: NumberType = new NumberType();
export interface BooleanType<B extends boolean = boolean> {
readonly constructor: typeof BooleanType<B>;
}
export class BooleanType<B extends boolean = boolean> extends PrimitiveType<B> {
static override readonly name = "boolean";
constructor(info?: Partial<TypeInfoState<B, BooleanType<B>>>) {
super("boolean", info);
}
}
export const boolean: BooleanType = new BooleanType();
export interface BigIntType<I extends bigint = bigint> {
readonly constructor: typeof BigIntType<I>;
}
export class BigIntType<I extends bigint = bigint> extends PrimitiveType<I> {
static override readonly name = "bigint";
constructor(info?: Partial<TypeInfoState<I, BigIntType<I>>>) {
super("bigint", info);
}
}
export const bigint: BigIntType = new BigIntType();
export interface SymbolType<S extends symbol = symbol> {
readonly constructor: typeof SymbolType<S>;
}
export class SymbolType<S extends symbol = symbol> extends PrimitiveType<S> {
static override readonly name = "symbol";
constructor(info?: Partial<TypeInfoState<S, SymbolType<S>>>) {
super("symbol", info);
}
wellknown(): WellKnownSymbolType {
return this.wellKnown();
}
@alias("wellknown")
wellKnown(): WellKnownSymbolType {
return new WellKnownSymbolType();
}
registered(): RegisteredSymbolType {
return new RegisteredSymbolType();
}
unique(): UniqueSymbolType {
return new UniqueSymbolType();
}
}
export const symbol: SymbolType = new SymbolType();
export interface UndefinedType {
readonly constructor: typeof UndefinedType;
}
export class UndefinedType extends PrimitiveType<undefined> {
static override readonly name = "undefined";
constructor(info?: Partial<TypeInfoState<undefined, UndefinedType>>) {
super("undefined", {
...info,
validator: (x: unknown): x is undefined => typeof x === "undefined",
});
}
}
const _undefined: UndefinedType = new UndefinedType();
export { _undefined as undefined };
export interface VoidType {
readonly constructor: typeof VoidType;
}
export class VoidType extends PrimitiveType<void> {
constructor(info?: Partial<TypeInfoState<void>>) {
super(
"void",
{
...info,
validator: (x: unknown): x is void => typeof x === "undefined",
},
);
}
}
const _void: VoidType = new VoidType();
export { _void as void };
export interface NullType {
readonly constructor: typeof NullType;
}
export class NullType extends Type<null> {
static override readonly name = "null";
constructor(info?: Partial<TypeInfoState<null>>) {
super("null", info);
}
override is(input: unknown): input is null {
return input === null;
}
}
const _null: NullType = new NullType();
export { _null as null };
export interface Any {
readonly constructor: typeof Any;
}
export class Any extends Type<any, any, any> {
static override readonly name = "any";
constructor(info?: Partial<TypeInfoState<any>>) {
super("any", { ...info, validator: (_): _ is any => true });
}
override as(input: unknown): any {
return input;
}
override validate(
input: unknown,
ctx: Context<any, any, any>,
): ValidationResult<any> {
return ctx.ok(input);
}
}
export const any: Any = new Any();
// @ts-expect-error readonly property reassignment
defaultTypeInfoState.type = any;
any.info.type(any);
export interface Unknown {
readonly constructor: typeof Unknown;
}
export class Unknown extends Type<unknown, any, unknown> {
static override readonly name = "unknown";
constructor(info?: Partial<TypeInfoState<unknown, Unknown>>) {
super("unknown", { ...info, validator: (_): _ is unknown => true });
}
override as(input: unknown): unknown {
return input;
}
override validate(
input: any,
ctx: Context<unknown, any, unknown>,
): ValidationResult<unknown> {
return ctx.ok(input);
}
}
export const unknown: Unknown = new Unknown();
export interface Never {
readonly constructor: typeof Never;
}
export class Never extends Type<never, any, never> {
static override readonly name = "never";
constructor(info?: Partial<TypeInfoState<never, Never>>) {
super("never", { ...info, validator: (_): _ is never => false });
}
override as(input: unknown): never {
throw new TypeError(`Type '${input}' is not assignable to type 'never'.`);
}
override validate(
input: unknown,
ctx: Context<never, any, never>,
): ValidationResult<never> {
void input;
return ctx.err`Type '${input}' is not assignable to type 'never'.`;
}
}
export const never: Never = new Never();
// ----------------------------------------------------------------------------
// Native Classes
// ----------------------------------------------------------------------------
export type DateLike = string | number | Date;
export interface DateType {
readonly constructor: typeof DateType;
}
export class DateType extends Type<Date> {
static override readonly name = "Date";
protected value: Date | null = null;
constructor(value: DateLike | null = null) {
if (value != null) {
value = new Date(value);
if (isNaN(value.getTime())) throw new TypeError("Invalid Date");
}
super(value === null ? "Date" : `Date<${value}>`);
this.value = value ?? null;
}
override is(input: unknown): input is Date {
return isDate(input) && (
this.value === null || this.value.getTime() === input.getTime()
);
}
}
export const date: DateType = new DateType();
export interface RegExpType {
readonly constructor: typeof RegExpType;
}
export class RegExpType extends Type<RegExp> {
static override readonly name = "RegExp";
constructor(info?: Partial<TypeInfoState<RegExp>>) {
super("RegExp", { ...info, validator: isRegExp });
}
}
export const regexp: RegExpType = new RegExpType();
export const regExp: RegExpType = regexp;
export interface ErrorType {
readonly constructor: typeof ErrorType;
}
export class ErrorType extends Type<Error> {
static override readonly name = "Error";
constructor(info?: Partial<TypeInfoState<Error>>) {
super("Error", { ...info, validator: isError });
}
}
export const error: ErrorType = new ErrorType();
export interface PromiseType<T = any> {
readonly constructor: typeof PromiseType<T>;
}
export class PromiseType<T = any> extends Type<Promise<T>> {
static override readonly name = "Promise";
constructor(info?: Partial<TypeInfoState<Promise<T>>>) {
super("Promise", { ...info, validator: isPromise });
}
}
export const promise: PromiseType = new PromiseType();
// ----------------------------------------------------------------------------
// Structured Data (Binary)
// ----------------------------------------------------------------------------
export interface ArrayBufferType {
readonly constructor: typeof ArrayBufferType;
}
export class ArrayBufferType extends Type<ArrayBuffer> {
static override readonly name = "ArrayBuffer";
constructor(info?: Partial<TypeInfoState<ArrayBuffer>>) {
super("ArrayBuffer", { ...info, validator: isArrayBuffer });
}
}
export const arrayBuffer: Type<ArrayBuffer> = new ArrayBufferType();
export interface DataViewType {
readonly constructor: typeof DataViewType;
}
export class DataViewType extends Type<DataView> {
static override readonly name = "DataView";
constructor(info?: Partial<TypeInfoState<DataView>>) {
super("DataView", { ...info, validator: isDataView });
}
}
export const dataView: Type<DataView> = new DataViewType();
export class TypedArrayType<
K extends TypedArrayTypeName = TypedArrayTypeName,
> extends Type<TypedArrayTypeMap[K]> {
constructor(
override readonly name: K | "TypedArray" = "TypedArray",
info?: Partial<TypeInfoState<TypedArrayTypeMap[K]>>,
) {
super(name ?? "", {
...info,
validator: (x: unknown): x is TypedArrayTypeMap[K] =>
!name || name === "TypedArray"
? isTypedArray(x)
: isTypedArray(x, name),
});
}
override is(input: unknown): input is TypedArrayTypeMap[K] {
if (this.name === "TypedArray") return isTypedArray(input);
return isTypedArray(input, this.name);
}
}
export interface Uint8ArrayType {
readonly constructor: typeof Uint8ArrayType;
}
export class Uint8ArrayType extends TypedArrayType<"Uint8Array"> {
static override readonly name = "Uint8Array";
constructor(info?: Partial<TypeInfoState<Uint8Array>>) {
super("Uint8Array", { ...info, validator: isUint8Array });
}
}
export const uint8Array: Uint8ArrayType = new Uint8ArrayType();
export interface Uint8ClampedArrayType {
readonly constructor: typeof Uint8ClampedArrayType;
}
export class Uint8ClampedArrayType extends TypedArrayType<"Uint8ClampedArray"> {
static override readonly name = "Uint8ClampedArray";
constructor(info?: Partial<TypeInfoState<Uint8ClampedArray>>) {
super("Uint8ClampedArray", { ...info, validator: isUint8ClampedArray });
}
}
export const uint8ClampedArray: Uint8ClampedArrayType =
new Uint8ClampedArrayType();
export interface Uint16ArrayType {
readonly constructor: typeof Uint16ArrayType;
}
export class Uint16ArrayType extends TypedArrayType<"Uint16Array"> {
static override readonly name = "Uint16Array";
constructor(info?: Partial<TypeInfoState<Uint16Array>>) {
super("Uint16Array", { ...info, validator: isUint16Array });
}
}
export const uint16Array: Uint16ArrayType = new Uint16ArrayType();
export interface Uint32ArrayType {
readonly constructor: typeof Uint32ArrayType;
}
export class Uint32ArrayType extends TypedArrayType<"Uint32Array"> {
static override readonly name = "Uint32Array";
constructor(info?: Partial<TypeInfoState<Uint32Array>>) {
super("Uint32Array", { ...info, validator: isUint32Array });
}
}
export const uint32Array: Uint32ArrayType = new Uint32ArrayType();
export interface Int8ArrayType {
readonly constructor: typeof Int8ArrayType;
}
export class Int8ArrayType extends TypedArrayType<"Int8Array"> {
static override readonly name = "Int8Array";
constructor(info?: Partial<TypeInfoState<Int8Array>>) {
super("Int8Array", { ...info, validator: isInt8Array });
}
}
export const int8Array: Int8ArrayType = new Int8ArrayType();
export interface Int16ArrayType {
readonly constructor: typeof Int16ArrayType;
}
export class Int16ArrayType extends TypedArrayType<"Int16Array"> {
static override readonly name = "Int16Array";
constructor(info?: Partial<TypeInfoState<Int16Array>>) {
super("Int16Array", { ...info, validator: isInt16Array });
}
}
export const int16Array: Int16ArrayType = new Int16ArrayType();
export interface Int32ArrayType {
readonly constructor: typeof Int32ArrayType;
}
export class Int32ArrayType extends TypedArrayType<"Int32Array"> {
static override readonly name = "Int32Array";
constructor(info?: Partial<TypeInfoState<Int32Array>>) {
super("Int32Array", { ...info, validator: isInt32Array });
}
}
export const int32Array: Int32ArrayType = new Int32ArrayType();
export interface Float16ArrayType {
readonly constructor: typeof Float16ArrayType;
}
export class Float16ArrayType extends TypedArrayType<"Float16Array"> {
static override readonly name = "Float16Array";
constructor(info?: Partial<TypeInfoState<Float16Array>>) {
super("Float16Array", { ...info, validator: isFloat16Array });
}
}
export const float16Array: Float16ArrayType = new Float16ArrayType();
export interface Float32ArrayType {
readonly constructor: typeof Float32ArrayType;
}
export class Float32ArrayType extends TypedArrayType<"Float32Array"> {
static override readonly name = "Float32Array";
constructor(info?: Partial<TypeInfoState<Float32Array>>) {
super("Float32Array", { ...info, validator: isFloat32Array });
}
}
export const float32Array: Float32ArrayType = new Float32ArrayType();
export interface Float64ArrayType {
readonly constructor: typeof Float64ArrayType;
}
export class Float64ArrayType extends TypedArrayType<"Float64Array"> {
static override readonly name = "Float64Array";
constructor(info?: Partial<TypeInfoState<Float64Array>>) {
super("Float64Array", { ...info, validator: isFloat64Array });
}
}
export const float64Array: Float64ArrayType = new Float64ArrayType();
export interface BigInt64ArrayType {
readonly constructor: typeof BigInt64ArrayType;
}
export class BigInt64ArrayType extends TypedArrayType<"BigInt64Array"> {
static override readonly name = "BigInt64Array";
constructor(info?: Partial<TypeInfoState<BigInt64Array>>) {
super("BigInt64Array", { ...info, validator: isBigInt64Array });
}
}
export const bigInt64Array: BigInt64ArrayType = new BigInt64ArrayType();
export interface BigUint64ArrayType {
readonly constructor: typeof BigUint64ArrayType;
}
export class BigUint64ArrayType extends TypedArrayType<"BigUint64Array"> {
static override readonly name = "BigUint64Array";
constructor(info?: Partial<TypeInfoState<BigUint64Array>>) {
super("BigUint64Array", { ...info, validator: isBigUint64Array });
}
}
export const bigUint64Array: BigUint64ArrayType = new BigUint64ArrayType();
// ----------------------------------------------------------------------------
// Control-flow and Iteration
// ----------------------------------------------------------------------------
export interface IteratorType<T = any> {
readonly constructor: typeof IteratorType<T>;
}
export class IteratorType<T = any> extends Type<Iterator<T>> {
static override readonly name = "Iterator";
constructor(info?: Partial<TypeInfoState<Iterator<T>>>) {
super("Iterator", info);
}
override is(input: unknown): input is Iterator<T> {
return typeof input === "object" && input != null &&
typeof (input as Iterator<T>).next === "function";
}
}
export const iterator: IteratorType = new IteratorType();
export interface IterableType<T = any> {
readonly constructor: typeof IterableType<T>;
}
export class IterableType<T = any> extends Type<Iterable<T>> {
static override readonly name = "Iterable<T>";
constructor(
protected valueType: Type<T> = any,
info?: Partial<TypeInfoState<Iterable<T>>>,
) {
super(`Iterable<${valueType}>`, info);
this.info.validator(
(input): input is Iterable<T> => {
if (!isIterable(input)) return false;
let i = 0;
for (const value of input) {
if (!valueType.is(value)) return false;
if (i++ > 1000) break; // Prevent infinite loops
}
return true;
},
);
}
}
export const iterable: IterableType = new IterableType();
export interface IterableIteratorType<
T = any,
TReturn = undefined,
TNext = undefined,
> {
readonly constructor: typeof IterableIteratorType<T, TReturn, TNext>;
}
export class IterableIteratorType<
T = any,
TReturn = undefined,
TNext = undefined,
> extends Type<IterableIterator<T, TReturn, TNext>> {
static override readonly name = "IterableIterator<T, TReturn, TNext>";
constructor(
readonly valueType: Type<T> = any,
info?: Partial<TypeInfoState<IterableIterator<T, TReturn, TNext>>>,
) {
super(`IterableIterator<${valueType}>`, info);
this.info.validator(
(input): input is IterableIterator<T, TReturn, TNext> => {
if (!isIterableIterator(input)) return false;
let i = 0;
for (const value of input) {
if (!valueType.is(value)) return false;
if (i++ > 1000) break; // Prevent infinite loops
}
return true;
},
);
}
}
export const iterableIterator: IterableIteratorType =
new IterableIteratorType();
export interface GeneratorType<T = unknown, TReturn = any, TNext = any> {
readonly constructor: typeof GeneratorType<T, TReturn, TNext>;
}
export class GeneratorType<T = unknown, TReturn = any, TNext = any>
extends Type<Generator<T, TReturn, TNext>> {
static override readonly name = "Generator<T, TReturn, TNext>";
constructor(
readonly valueType: Type<T> = any,
info?: Partial<TypeInfoState<Generator<T, TReturn, TNext>>>,
) {
super(`Generator<${valueType}>`, info);
this.info.validator(
(input): input is Generator<T, TReturn, TNext> => {
if (!isGenerator(input)) return false;
let i = 0;
for (const value of input) {
if (!valueType.is(value)) return false;
if (i++ > 1000) break; // Prevent infinite loops
}
return true;
},
);
}
}
export const generator: GeneratorType = new GeneratorType();
export interface GeneratorFunctionType {
readonly constructor: typeof GeneratorFunctionType;
}
export class GeneratorFunctionType extends Type<GeneratorFunction> {
static override readonly name = "GeneratorFunction";
constructor(info?: Partial<TypeInfoState<GeneratorFunction>>) {
super("GeneratorFunction", { ...info, validator: isGeneratorFunction });
}
}
export const generatorFunction: GeneratorFunctionType =
new GeneratorFunctionType();
export class AsyncGeneratorType<T = unknown, TReturn = any, TNext = any>
extends Type<AsyncGenerator<T, TReturn, TNext>> {
static override readonly name = "AsyncGenerator<T, TReturn, TNext>";
constructor(
info?: Partial<TypeInfoState<AsyncGenerator<T, TReturn, TNext>>>,
) {
super("AsyncGenerator<T, TReturn, TNext>", {
...info,
validator: isAsyncGenerator as any,
});
}
}
export const asyncGenerator: AsyncGeneratorType = new AsyncGeneratorType();
export interface AsyncGeneratorFunctionType {
readonly constructor: typeof AsyncGeneratorFunctionType;
}
export class AsyncGeneratorFunctionType extends Type<AsyncGeneratorFunction> {
static override readonly name = "AsyncGeneratorFunction";
constructor(info?: Partial<TypeInfoState<AsyncGeneratorFunction>>) {
super("AsyncGeneratorFunction", {
...info,
validator: isAsyncGeneratorFunction,
});
}
}
export const asyncGeneratorFunction: Type<AsyncGeneratorFunction> =
new AsyncGeneratorFunctionType();
export interface AsyncIterableType<
T = any,
TReturn = undefined,
TNext = undefined,
> {
readonly constructor: typeof AsyncIterableType<T, TReturn, TNext>;
}
export class AsyncIterableType<T = any, TReturn = undefined, TNext = undefined>
extends Type<AsyncIterable<T, TReturn, TNext>> {
static override readonly name = "AsyncIterable<T, TReturn, TNext>";
constructor(
info?: Partial<TypeInfoState<AsyncIterable<T, TReturn, TNext>>>,
) {
// TODO: add support for valueType (and asynchronous validation ... ?)
super(`AsyncIterable<T, TReturn, TNext>`, {
...info,
validator: isAsyncIterable,
});
}
}
export const asyncIterable: AsyncIterableType = new AsyncIterableType();
export interface AsyncIterableIteratorType<
T = any,
TReturn = undefined,
TNext = undefined,
> {
readonly constructor: typeof AsyncIterableIteratorType<T, TReturn, TNext>;
}
export class AsyncIterableIteratorType<
T = any,
TReturn = undefined,
TNext = undefined,
> extends Type<AsyncIterableIterator<T, TReturn, TNext>> {
static override readonly name = "AsyncIterableIterator<T, TReturn, TNext>";
constructor(
info?: Partial<TypeInfoState<AsyncIterableIterator<T, TReturn, TNext>>>,
) {
super("AsyncIterableIterator<T, TReturn, TNext>", {
...info,
validator: isAsyncIterableIterator,
});
}
}
export const asyncIterableIterator: AsyncIterableIteratorType =
new AsyncIterableIteratorType();
export interface AsyncIteratorType<
T = any,
TReturn = undefined,
TNext = undefined,
> {
readonly constructor: typeof AsyncIteratorType<T, TReturn, TNext>;
}
export class AsyncIteratorType<T = any, TReturn = undefined, TNext = undefined>
extends Type<AsyncIterator<T>> {
static override readonly name = "AsyncIterator<T, TReturn, TNext>";
constructor(info?: Partial<TypeInfoState<AsyncIterator<T, TReturn, TNext>>>) {
super("AsyncIterator<T, TReturn, TNext>", {
...info,
validator: isAsyncIterator,
});
}
}
export const asyncIterator: AsyncIteratorType = new AsyncIteratorType();
export interface AsyncFunction<T = any> extends Function {
(...args: any[]): Promise<T>;
}
export interface AsyncFunctionType<T = any> {
readonly constructor: typeof AsyncFunctionType<T>;
}
export class AsyncFunctionType<T = any> extends Type<AsyncFunction<T>> {
static override readonly name = "AsyncFunction";
constructor(info?: Partial<TypeInfoState<AsyncFunction<T>>>) {
super("AsyncFunction", { ...info, validator: isAsyncFunction });
}
override as(input: unknown): AsyncFunction<T> {
if (this.is(input)) return input;
if (isFunction(input)) {
return async function (this: unknown, ...args) {
const result = FunctionPrototypeApply(
input as AsyncFunction,
this,
args,
);
return await Promise.resolve(result);
};
}
throw new TypeError("Expected an AsyncFunction or a Function");
}
}
export const asyncFunction: AsyncFunctionType = new AsyncFunctionType();
export interface FunctionType {
readonly constructor: typeof FunctionType;
}
export class FunctionType extends Type<Function> {
static override readonly name = "Function";
constructor(info?: Partial<TypeInfoState<Function>>) {
super("Function", { ...info, validator: isFunction });
}
}
export const fn: FunctionType = new FunctionType();
export { fn as function };
// ----------------------------------------------------------------------------
// Keyed Collections
// ----------------------------------------------------------------------------
export interface MapType<K, V> {
<K2 extends K, V2 extends V>(
keyType?: Type<K2>,
valueType?: Type<V2>,
info?: Partial<TypeInfoState<Map<K2, V2>>>,
): MapType<K2, V2>;
readonly constructor: typeof MapType<K, V>;
}
export class MapType<K, V> extends Type<Map<K, V>> {
constructor(
protected keyType: Type<K> = any,
protected valueType: Type<V> = any,
info?: Partial<TypeInfoState<Map<K, V>>>,
) {
super(`Map<${keyType}, ${valueType}>`, { ...info, validator: isMap });
}
override is(input: unknown): input is Map<K, V> {
if (isMap(input)) {
for (const [key, value] of input) {
if (!this.keyType.is(key) || !this.valueType.is(value)) return false;
}
}
return false;
}
override validate(
input: unknown,
ctx: Context,
): ValidationResult<Map<K, V>> {
const { keyType, valueType } = this;
if (isMap<K, V>(input)) {
let i = 0;
for (const [k, v] of input) {
if (!keyType.is(k)) {
return ctx
.err`Key type '${k}' is not assignable to map key type '${keyType}'.`;
}
if (!valueType.is(v)) {
const vc = ctx.with(Type.inspect(k));
return vc.err`Type '${v}' is not assignable to type '${valueType}'.`;
}
i++;
}
return ctx.ok(input);
}
return ctx.err`Expected Map<${keyType}, ${valueType}>`;
}
}
export const map: MapType<any, any> = new MapType();
export interface SetType<T = any> {
readonly constructor: typeof SetType<T>;
}
export class SetType<T = any> extends Type<Set<T>> {
constructor(
protected valueType: Type<T> = any,
info?: Partial<TypeInfoState<Set<T>>>,
) {
super(`Set<${valueType}>`, { ...info, validator: isSet });
}
override is(input: unknown): input is Set<T> {
if (isSet(input)) {
for (const value of input) {
if (!this.valueType.is(value)) return false;
}
}
return false;
}
override validate(
input: unknown,
ctx: Context<Set<T>>,
): ValidationResult<Set<T>> {
const { valueType } = this;
if (isSet<T>(input)) {
let i = 0;
for (const v of input) {
if (!valueType.is(v)) {
const valueCtx = ctx.with(`[${i}]`);
return valueCtx
.err`Type '${v}' is not assignable to type '${valueType}' (index ${i})`;
}
i++;
}
return ctx.ok(input);
}
return ctx.err`Type '${input}' is not assignable to type '${this}'`;
}
}
export const set: SetType<any> = new SetType();
export interface WeakKeyType {
readonly constructor: typeof WeakKeyType;
}
export class WeakKeyType extends Type<WeakKey> {
static override readonly name = "WeakKey";
constructor(info?: Partial<TypeInfoState<WeakKey>>) {
super("WeakKey", { ...info, validator: isWeakKey });
}
override validate(
input: unknown,
ctx: Context<WeakKey>,
): ValidationResult<WeakKey>;
override validate(input: unknown, ctx: Context): ValidationResult {
if (this.is(input)) {
return ctx.ok(input);
} else {
return ctx
.err`Expected a WeakKey value, which includes all objects/arrays/functions. ES2023+ runtimes also support non-registered symbols (not created with Symbol.for) as weak keys.`;
}
}
}
export const weakKey: WeakKeyType = new WeakKeyType();
export class WeakMapType<K extends WeakKey = WeakKey, V = any>
extends Type<WeakMap<K, V>> {
static override readonly name = "WeakMap";
constructor(info?: Partial<TypeInfoState<WeakMap<K, V>>>) {
super("WeakMap", { ...info, validator: isWeakMap });
}
}
export const weakMap: WeakMapType = new WeakMapType();
export interface WeakSetType<T extends WeakKey = WeakKey> {
readonly constructor: typeof WeakSetType<T>;
}
export class WeakSetType<T extends WeakKey = WeakKey> extends Type<WeakSet<T>> {
static override readonly name = "WeakSet";
constructor(info?: Partial<TypeInfoState<WeakSet<T>>>) {
super("WeakSet", { ...info, validator: isWeakSet });
}
}
export const weakSet: WeakSetType = new WeakSetType();
export interface WeakRefType<T extends WeakKey = WeakKey> {
readonly constructor: typeof WeakRefType<T>;
}
export class WeakRefType<T extends WeakKey = WeakKey> extends Type<WeakRef<T>> {
static override readonly name = "WeakRef";
constructor(
protected valueType: Type<T> = weakKey as unknown as Type<T>,
info?: Partial<TypeInfoState<WeakRef<T>>>,
) {
super("WeakRef", { ...info, validator: isWeakRef });
if (valueType !== (weakKey as unknown as Type<T>)) {
this.info.constraint(
"valueType",
valueType.is.bind(valueType),
`Type '{input}' is not assignable to type '${valueType}'`,
);
}
}
}
export const weakRef: WeakRefType = new WeakRefType();
// ----------------------------------------------------------------------------
// Composite and Higher-Order Types
// ----------------------------------------------------------------------------
export interface UnionType<A, B> {
readonly constructor: typeof UnionType<A, B>;
}
export class UnionType<A, B> extends Type<A | B> {
static override readonly name = "Union<A, B>";
constructor(protected left: Type<A>, protected right: Type<B>) {
super(`${left} | ${right}`);
}
override is(input: unknown): input is A | B {
return this.left.is(input) || this.right.is(input);
}
override as(input: unknown): A | B {
if (this.left.is(input)) return this.left.as(input);
if (this.right.is(input)) return this.right.as(input);
throw new ValidationError(`Invalid value for ${this}`);
}
override validate(v: unknown, ctx: Context<A | B>): ValidationResult<A | B>;
override validate(input: unknown, ctx: Context): ValidationResult {
const a = this.left.validate(input, ctx);
if (a.success) return a;
const b = this.right.validate(input, ctx);
if (b.success) return b;
return ctx.err`Type '${input}' is not assignable to union type '${this}'`;
}
}
export interface IntersectionType<A, B> {
readonly constructor: typeof IntersectionType<A, B>;
}
export class IntersectionType<A, B> extends Type<A & B> {
constructor(
protected left: Type<A>,
protected right: Type<B>,
info?: Partial<TypeInfoState<A & B>>,
) {
super(`${left} & ${right}`, info);
}
override is(input: unknown): input is A & B {
return this.left.is(input) && this.right.is(input);
}
override as(input: unknown): A & B {
const a = this.left.as(input), b = this.right.as(input);
if ((isFunction(a) || isObject(a)) && (isFunction(b) || isObject(b))) {
return Object.assign({}, a, b);
} else {
return b as A & B;
}
}
override validate(
input: unknown,
ctx: Context<A & B>,
): ValidationResult<A & B>;
override validate(input: unknown, ctx: Context): ValidationResult {
const a = this.left.validate(input, ctx);
if (!a.success) return a;
const b = this.right.validate(input, ctx);
if (!b.success) return b;
if (
typeof a.value === "object" && a.value !== null &&
typeof b.value === "object" && b.value !== null
) {
return ctx.ok(Object.assign({}, a.value, b.value));
}
return ctx.ok(b.value as A & B);
}
}
export interface ExcludeType<T, U> {
readonly constructor: typeof ExcludeType<T, U>;
}
export class ExcludeType<T, U> extends Type<T> {
static override readonly name = "Exclude<T, U>";
constructor(
protected base: Type<T>,
protected other: Type<U>,
info?: Partial<TypeInfoState<T>>,
) {
super(`Exclude<${base}, ${other}>`, info);
}
override is<B>(input: B): input is Exclude<Extract<B, T>, U> {
return this.base.is(input) && !this.other.is(input);
}
override as<B>(input: B): Exclude<Extract<B, T>, U> {
const t = this.base.as(input);
if (!this.other.is(t)) return t as Exclude<Extract<B, T>, U>;
throw new TypeError(
this.context
.err`Type '${input}' is not assignable to type '${this}'. Reason: types assignable to '${this.other}' are excluded.`
.error,
);
}
override validate(input: unknown, ctx: Context): ValidationResult<T> {
const res = this.base.validate(input, ctx);
if (res.success) {
if (this.other.is(res.value)) {
return ctx.err`Type '${input}' is not assignable to type '${this}'`;
}
return res;
}
return res;
}
}
export interface MappedType<T, U> {
readonly constructor: typeof MappedType<T, U>;
}
export class MappedType<T, U> extends Type<U> {
static override readonly name = "Mapped<T, U>";
constructor(protected inner: Type<T>, protected mapper: (input: T) => U) {
super(`Mapped<${inner}, ${mapper}>`);
}
override as(input: unknown): U {
const t = this.inner.as(input);
return this.mapper(t);
}
override validate(input: unknown, ctx: Context): ValidationResult<U> {
const res = this.inner.validate(input, ctx);
if (res.success) {
try {
return ctx.ok(this.mapper(res.value));
} catch (e) {
return ctx.err`MappedType validation failed with error ${e}`;
}
}
return res as ValidationResult<U>;
}
}
export interface FilterType<T> {
readonly constructor: typeof FilterType<T>;
}
export class FilterType<T> extends Type<T> {
static override readonly name = "Filter<T>";
constructor(
protected inner: Type<T>,
protected predicate: (input: T) => boolean,
) {
super(`Filter<${inner}, ${predicate}>`);
}
override is(input: unknown): input is T {
try {
const t = this.inner.as(input);
return this.predicate(t);
} catch {
return false;
}
}
override as(input: unknown): T {
const t = this.inner.as(input);
if (this.predicate(t)) return t;
throw new TypeError(`Filter predicate failed on ${t}`);
}
override validate(input: unknown, ctx: Context): ValidationResult<T> {
const res = this.inner.validate(input, ctx);
if (res.success) {
if (this.predicate(res.value)) return res;
return ctx.err`Filter predicate failed on ${res.value}`;
}
return res;
}
}
export interface TransmuteType<T, U> {
readonly constructor: typeof TransmuteType<T, U>;
}
export class TransmuteType<T, U> extends Type<U> {
static override readonly name = "Transmute<T, U>";
constructor(
protected inner: Type<T>,
protected transformer: (input: T) => U,
) {
super(`Transmute<${inner}, ${transformer}>`);
}
override is(input: unknown): input is U {
try {
const t = this.inner.as(input);
this.transformer(t);
return true;
} catch {
return false;
}
}
override as(input: unknown): U {
const t = this.inner.as(input);
return this.transformer(t);
}
override validate(input: unknown, ctx: Context): ValidationResult<U> {
const res = this.inner.validate(input, ctx);
if (res.success) {
try {
return ctx.ok(this.transformer(res.value));
} catch (e) {
return ctx.err(
`Transmutation failed: ${e instanceof Error ? e.message : e}`,
);
}
}
return res as ValidationResult<U>;
}
}
export interface CastType<T, U> {
readonly constructor: typeof CastType<T, U>;
}
export class CastType<T, U> extends Type<U> {
static override readonly name = "Cast<T, U>";
constructor(
protected inner: Type<T>,
protected caster: (input: T) => U,
readonly predicate: (it: unknown) => it is U = (_): _ is U => true,
) {
super(`Cast<${inner}, ${caster}>`);
}
override is(input: unknown): input is U {
return this.inner.is(input) && this.predicate(this.caster(input));
}
override as(input: unknown): U {
const t = this.inner.as(input);
return this.caster(t);
}
override validate(input: unknown, ctx: Context): ValidationResult<U> {
const res = this.inner.validate(input, ctx);
if (res.success) {
try {
return ctx.ok(this.caster(res.value));
} catch (e) {
return ctx.err(
`Cast failed: ${e instanceof Error ? e.message : e}`,
);
}
}
return res as ValidationResult<U>;
}
}
export interface RefineType<T, U extends T> {
readonly constructor: typeof RefineType<T, U>;
}
export class RefineType<T, U extends T> extends Type<U> {
static override readonly name = "Refine<T, U>";
static from<T, U extends T, I extends Type<T>>(
inner: I,
refinement: (it: T) => it is U,
info?: Partial<TypeInfoState<T & U, RefineType<T, U>>>,
): RefineType<T, U> & I {
const wrapped = new this(inner, refinement, info) as RefineType<T, U> & I;
ObjectSetPrototypeOf(wrapped, inner);
return wrapped;
}
constructor(
protected inner: Type<T>,
protected refinement: (it: T) => it is U,
info?: Partial<TypeInfoState<T & U, RefineType<T, U>>>,
) {
super(`Refine<${inner}, ${refinement}>`, info);
}
override is(input: unknown): input is U {
return this.inner.is(input) && this.refinement(input as T);
}
override as(input: unknown): U {
const t = this.inner.as(input);
if (this.refinement(t)) return t;
throw new TypeError(`Refinement failed on ${t}`);
}
override validate(input: unknown, ctx: Context): ValidationResult<U> {
const res = this.inner.validate(input, ctx);
if (res.success) {
if (this.refinement(res.value)) return res as ValidationResult<U>;
return ctx.err(`Refinement failed on ${res.value}`);
}
return res as ValidationResult<U>;
}
}
export class BrandType<T, K extends PropertyKey = "__brand", U = never>
extends Type<T & { readonly [P in K]: U }> {
static override readonly name = "Brand<T, K, U>";
constructor(
protected inner: Type<T>,
protected key: K = "__brand" as K,
protected brandValue: U = null! as U,
) {
const keyStr = Type.inspect(key);
const brandStr = Type.inspect(brandValue);
super(`Brand<${inner}, ${keyStr}, ${brandStr}>`);
}
override is(input: unknown): input is T & { [P in K]: U } {
return this.inner.is(input) && (input as any)[this.key] === this.brandValue;
}
override as(input: unknown): T & { [P in K]: U } {
const t = this.inner.as(input);
if ((t as any)[this.key] === this.brandValue) {
return t as T & { [P in K]: U };
}
throw new TypeError(
`Brand check failed: expected property ${
String(this.key)
} to be ${this.brandValue}`,
);
}
override validate(
input: unknown,
ctx: Context,
): ValidationResult<T & { [P in K]: U }> {
const res = this.inner.validate(input, ctx);
if (res.success) {
if ((res.value as any)[this.key] === this.brandValue) {
return res as ValidationResult<T & { [P in K]: U }>;
}
return ctx.err(
`Brand check failed: property ${
String(this.key)
} is not ${this.brandValue}`,
);
}
return res as ValidationResult<T & { [P in K]: U }>;
}
}
// ----------------------------------------------------------------------------
// Symbol Types
// ----------------------------------------------------------------------------
export interface WellKnownSymbolType {
readonly constructor: typeof WellKnownSymbolType;
}
export class WellKnownSymbolType extends PrimitiveType<WellKnownSymbol> {
static override readonly name = "WellKnownSymbol";
static readonly symbols = [
SymbolAsyncDispose,
SymbolAsyncIterator,
SymbolDispose,
SymbolHasInstance,
SymbolIsConcatSpreadable,
SymbolIterator,
SymbolMatch,
SymbolMatchAll,
SymbolMetadata,
SymbolReplace,
SymbolSearch,
SymbolSpecies,
SymbolSplit,
SymbolToPrimitive,
SymbolToStringTag,
SymbolUnscopables,
] as const satisfies ReadonlyArray<WellKnownSymbol>;
static {
ObjectFreeze(this.symbols);
for (const symbol of this.symbols) {
const name = (
symbol.description ?? symbol.toString()
).replace(/Symbol\.(\w+)/, "$1");
const type = new this({ name, symbol }).refine(
(x) => x === symbol,
`Expected '{input}' to be the well-known symbol '${symbol.toString()}'`,
);
type.info.name(`Symbol.${name}` as const);
(this as typeof WellKnownSymbolType & Record<string, any>)[name] = type;
}
}
declare static readonly asyncDispose: Type<SymbolAsyncDispose>;
declare static readonly asyncIterator: Type<SymbolAsyncIterator>;
declare static readonly dispose: Type<SymbolDispose>;
declare static readonly hasInstance: Type<SymbolHasInstance>;
declare static readonly isConcatSpreadable: Type<SymbolIsConcatSpreadable>;
declare static readonly iterator: Type<SymbolIterator>;
declare static readonly match: Type<SymbolMatch>;
declare static readonly matchAll: Type<SymbolMatchAll>;
declare static readonly metadata: Type<SymbolMetadata>;
declare static readonly replace: Type<SymbolReplace>;
declare static readonly search: Type<SymbolSearch>;
declare static readonly species: Type<SymbolSpecies>;
declare static readonly split: Type<SymbolSplit>;
declare static readonly toPrimitive: Type<SymbolToPrimitive>;
declare static readonly toStringTag: Type<SymbolToStringTag>;
declare static readonly unscopables: Type<SymbolUnscopables>;
constructor(
info?: Partial<TypeInfoState<WellKnownSymbol, WellKnownSymbolType>>,
) {
super("symbol", { ...info, validator: isWellKnownSymbol });
this.info.name(info?.name ?? "WellKnownSymbol");
}
override toString(): string {
return this.info.getName() || "WellKnownSymbol";
}
}
export const wellKnownSymbol: WellKnownSymbolType = new WellKnownSymbolType();
export interface RegisteredSymbolType<
T extends RegisteredSymbol = RegisteredSymbol,
> {
readonly constructor: typeof RegisteredSymbolType<T>;
}
export class RegisteredSymbolType<
T extends RegisteredSymbol = RegisteredSymbol,
> extends Type<T> {
static override readonly name = "RegisteredSymbol";
constructor(info?: Partial<TypeInfoState<T, RegisteredSymbolType<T>>>) {
super("RegisteredSymbol", info);
this.info.validator(
isRegisteredSymbol as unknown as (it: unknown) => it is T,
);
}
}
export const registeredSymbol: RegisteredSymbolType =
new RegisteredSymbolType();
export interface UniqueSymbolType<
T extends UniqueSymbol = UniqueSymbol,
> {
readonly constructor: typeof UniqueSymbolType<T>;
}
export class UniqueSymbolType<
T extends UniqueSymbol = UniqueSymbol,
> extends Type<T> {
static override readonly name = "UniqueSymbol";
constructor(info?: Partial<TypeInfoState<T, UniqueSymbolType<T>>>) {
super("UniqueSymbol", info);
this.info.validator(isUniqueSymbol as unknown as (it: unknown) => it is T);
}
}
export const uniqueSymbol: UniqueSymbolType = new UniqueSymbolType();
// ----------------------------------------------------------------------------
// Object and Array Types
// ----------------------------------------------------------------------------
export interface ObjectType<T extends Record<string, Type<any>>> {
readonly constructor: typeof ObjectType<T>;
}
export class ObjectType<const T extends Record<string, Type<any>>>
extends Type<Type.infer<T>> {
static override readonly name = "Object<T>";
@lru({ maxSize: 1024 })
static render<T extends Record<string, unknown>>(
schema: T,
options: RenderOptions = { ...defaultRenderOptions },
): string {
const opt = { ...defaultRenderOptions, ...options };
const keys = Object.keys(schema) as (keyof T)[];
let multiline = false, sp = " ", s = "";
s += "{";
for (let i = 0; i < keys.length; i++) {
const key = keys[i], type = schema[key];
const def = type instanceof Type ? type.toString() : Type.inspect(type);
if (multiline || s.length > opt.lineWidth - 2) {
multiline = true;
sp = opt.useTabs ? "\t" : " ".repeat(opt.indentWidth);
sp = sp.repeat(++opt.indent);
sp = "\n" + sp;
}
s += sp;
let k = key.toString();
if (!/^[$_\p{ID_Start}][$_\p{ID_Continue}\u{200C}\u{200D}]*$/u.test(k)) {
k = JSON.stringify(k);
}
if (opt.singleQuote) k = k.replace(/"/g, "'");
s += `${k}: ${def}`;
if (i === keys.length - 1) {
if (multiline) {
s += `${opt.trailingComma ? "," : ""}\n`;
} else {
s += " ";
}
} else {
s += ",";
}
}
s += "}";
return s;
}
constructor(
readonly schema: T,
info?: Partial<TypeInfoState<Type.infer<T>>>,
) {
super("object", info);
}
override is(input: unknown): input is Type.infer<T> {
if (typeof input !== "object" || input === null) return false;
for (const key in this.schema) {
const checker = this.schema[key];
if (!(checker && checker instanceof Type)) continue;
if (!checker.is((input as any)[key])) return false;
}
return true;
}
override as(input: unknown): Type.infer<T> {
if (typeof input !== "object" || input === null) {
throw new TypeError(`Expected object, got ${typeof input}`);
}
const result = { __proto__: null } as unknown as Type.infer<T>;
for (const key in this.schema) {
const checker = this.schema[key];
if (!(checker && checker instanceof Type)) continue;
result[key] = checker.as(input[key as keyof typeof input]);
}
return result;
}
override validate(
input: unknown,
ctx: Context<Type.infer<T>>,
): ValidationResult<Type.infer<T>> {
if (typeof input !== "object" || input === null) {
return ctx.err`Expected an object of type ${this}, got ${input}`;
}
const result: any = {};
for (const key in this.schema) {
const checker = this.schema[key];
if (!(checker && checker instanceof Type)) continue;
const sub = ctx.with(key);
const res = checker.validate(input[key as keyof typeof input], sub);
if (!res.success) {
const error = new TypeError(
`Failed to validate property ${key} (type: ${checker}) of ${this}`,
{ cause: res.error },
);
return ctx.err(error, sub.path);
}
result[key] = res.value;
}
return ctx.ok(result);
}
override toJSON(): object {
const result: any = {};
for (const key in this.schema) {
const checker = this.schema[key];
if (!(checker && checker instanceof Type)) continue;
result[key] = checker.toJSON();
}
return result;
}
override toString(): string {
if (this.name !== "object") return this.name;
// @ts-expect-error readonly property
return this.name = ObjectType.render(this.schema);
}
}
export function object<const T extends Record<string, Type<any>>>(
schema: T,
): ObjectType<T> {
return new ObjectType(schema);
}
export interface RecordType<K extends PropertyKey, V> {
readonly constructor: typeof RecordType<K, V>;
}
export class RecordType<K extends PropertyKey, V> extends Type<Record<K, V>> {
static override readonly name = "Record<K, V>";
constructor(
readonly keyType: Type<K>,
readonly valueType: Type<V>,
info?: Partial<TypeInfoState<Record<K, V>>>,
) {
super(`Record<${keyType}, ${valueType}>`, info);
}
override is(input: unknown): input is Record<K, V> {
if (typeof input !== "object" || input === null) return false;
for (const key in input) {
if (!this.keyType.is(key)) return false;
if (!this.valueType.is((input as any)[key])) return false;
}
return true;
}
override as(input: unknown): Record<K, V> {
if (typeof input !== "object" || input === null) {
throw new TypeError(`Expected object, got ${typeof input}`);
}
const result = { __proto__: null } as unknown as Record<K, V>;
for (const key in input) {
const k = this.keyType.as(key);
const v = this.valueType.as((input as any)[k]);
result[k] = v;
}
return result;
}
override validate(
input: unknown,
ctx: Context<Record<K, V>>,
): ValidationResult<Record<K, V>> {
if (typeof input !== "object" || input === null) {
return ctx.err(`Expected object, got ${typeof input}`);
}
const result: any = {};
for (const key in input) {
const k = this.keyType.validate(key, ctx as Context);
if (!k.success) return ctx.err(k.error);
const v = this.valueType.validate(
(input as any)[k.value],
ctx.with(k.value) as Context,
);
if (!v.success) return ctx.err(v.error);
result[k.value] = v.value;
}
return ctx.ok(result);
}
}
export function record<K extends PropertyKey, V>(
keyType: Type<K>,
valueType: Type<V>,
): RecordType<K, V> {
return new RecordType(keyType, valueType);
}
export type Tuple<T = any> = readonly [] | readonly [T, ...T[]];
export interface TupleType<T extends Tuple<Type<any>>> {
readonly constructor: typeof TupleType<T>;
}
export class TupleType<const T extends Tuple<Type<any>>>
extends Type<InferTuple<T>> {
static override readonly name = "Tuple<T>";
constructor(
protected types: T,
info?: Partial<TypeInfoState<InferTuple<T>>>,
) {
super(`Tuple<${types}>`, info);
}
override is(input: unknown): input is InferTuple<T> {
if (!Array.isArray(input)) return false;
if (input.length !== this.types.length) return false;
for (let i = 0; i < this.types.length; i++) {
if (!this.types[i].is(input[i])) return false;
}
return true;
}
override as(input: unknown): InferTuple<T> {
if (!Array.isArray(input)) {
throw new TypeError(`Expected array, got ${typeof input}`);
}
const result = [] as unknown as InferTuple<T>;
for (let i = 0; i < this.types.length; i++) {
result[i] = this.types[i].as(input[i]);
}
return result;
}
override validate(
input: unknown,
ctx: Context,
): ValidationResult<InferTuple<T>> {
if (!Array.isArray(input)) {
return ctx.err(`Expected array, got ${typeof input}`);
}
const result: any[] = [];
for (let i = 0; i < this.types.length; i++) {
const subContext = ctx.with(String(i));
const res = this.types[i].validate(input[i], subContext);
if (!res.success) {
return ctx.err(`Error at index ${i}: ${res.error}`);
}
result.push(res.value);
}
return ctx.ok(result);
}
}
export function tuple<const T extends Tuple<Type<any>>>(
...types: T
): TupleType<T> {
return new TupleType(types);
}
export interface ArrayType<T> {
readonly constructor: typeof ArrayType<T>;
}
export class ArrayType<T> extends Type<T[]> {
static override readonly name = "Array<T>";
constructor(
protected elementType: Type<T> = any,
info?: Partial<TypeInfoState<T[]>>,
) {
const et = elementType.is.bind(elementType);
const validator = (x: unknown): x is T[] => isArray(x, et);
super(`Array<${elementType}>`, { ...info, validator });
}
override as(input: unknown): T[] {
if (!isArray(input)) {
throw new TypeError(`Expected array, got ${typeof input}`);
}
return input.map((el) => this.elementType.as(el));
}
override validate(input: unknown, ctx: Context): ValidationResult<T[]> {
if (!isArray(input)) {
return ctx.err(`Expected array, got ${typeof input}`);
}
const result: T[] = [];
for (let i = 0; i < input.length; i++) {
const subContext = ctx.with(String(i));
const res = this.elementType.validate(input[i], subContext);
if (!res.success) {
return ctx.err`Error at index ${i}: ${res.error}`;
}
result.push(res.value);
}
return ctx.ok(result);
}
}
export function array<T>(elementType: Type<T>): ArrayType<T> {
return new ArrayType(elementType);
}
// ----------------------------------------------------------------------------
// Optional / Nullable Types
// ----------------------------------------------------------------------------
export interface OptionalType<T> {
readonly constructor: typeof OptionalType<T>;
}
export class OptionalType<T> extends Type<T | undefined> {
constructor(
protected inner: Type<T>,
info?: Partial<TypeInfoState<T | undefined>>,
) {
const validator = (x: unknown): x is T | undefined =>
x === void 0 || (this.inner.is(x) && info?.validator?.(x) !== false);
super(`${inner} | undefined`, { ...info, optional: true, validator });
}
override as(input: unknown): T | undefined {
return input === void 0 ? void 0 : this.inner.as(input);
}
override validate(
input: unknown,
ctx: Context,
): ValidationResult<T | undefined> {
if (input === void 0) return ctx.ok(void 0);
return this.inner.validate(input, ctx);
}
}
export function optional<T>(inner: Type<T>): OptionalType<T> {
return new OptionalType(inner);
}
export interface NullableType<T> {
readonly constructor: typeof NullableType<T>;
}
export class NullableType<T> extends Type<T | null> {
constructor(protected inner: Type<T>) {
super(`${inner} | null`, { nullable: true });
}
override is(input: unknown): input is T | null {
return input === null || this.inner.is(input);
}
override as(input: unknown): T | null {
return input === null ? null : this.inner.as(input);
}
override validate(input: unknown, ctx: Context): ValidationResult<T | null> {
if (input === null) return ctx.ok(null);
return this.inner.validate(input, ctx);
}
}
export function nullable<T>(inner: Type<T>): NullableType<T> {
return new NullableType(inner);
}
export interface NonNullableType<T> {
readonly constructor: typeof NonNullableType<T>;
}
export class NonNullableType<T> extends Type<NonNullable<T>> {
constructor(protected inner: Type<T>) {
super(`NonNullable<${inner}>`, { nullable: false });
}
override is(input: unknown): input is NonNullable<T> {
return input != null && this.inner.is(input);
}
override as(input: unknown): NonNullable<T> {
if (input === null || input === undefined) {
throw new Error(`Value is null or undefined for nonNullable type`);
}
return this.inner.as(input) as NonNullable<T>;
}
override validate(
input: unknown,
ctx: Context,
): ValidationResult<NonNullable<T>> {
if (!this.is(input)) {
return ctx.err`Cannot assign a value of ${input} to ${this}`;
}
return this.inner.validate(input, ctx) as ValidationResult<
NonNullable<T>
>;
}
}
export function notnull<T>(inner: Type<T>): NonNullableType<T> {
return new NonNullableType(inner);
}
// ----------------------------------------------------------------------------
// Registry and Refs
// ----------------------------------------------------------------------------
export class RefType<
K extends string,
T extends (K extends keyof R ? R[K]
: K extends keyof R["types"] ? R["types"][K]
: any),
R extends Registry<any, RegistryTypes> = Registry<any, RegistryTypes>,
> extends Type<T> {
constructor(
protected refName: string,
protected registry: Registry<any, any>,
) {
super(refName);
}
override is(input: unknown): input is T {
return this.registry.get(this.refName).is(input);
}
override as(input: unknown): T {
return this.registry.get(this.refName).as(input);
}
override validate(input: unknown, ctx: Context): ValidationResult<T> {
return this.registry.get(this.refName).validate(input, ctx);
}
}
export function ref<
R extends Registry,
K extends string = string & keyof R,
T extends R["types"][keyof R["types"]] = K extends keyof R["types"]
? R["types"][K]
: any,
>(name: K, registry: R): Type<T> {
return new RefType(name, registry);
}
export interface RegistryTypes {
[name: string]: Type<any> | ((...args: any[]) => Type<any>) | {};
}
export class Registry<
Name extends string = string,
const Types extends RegistryTypes = RegistryTypes,
> {
static create<
Name extends string,
const Types extends RegistryTypes,
>(name: Name, types: Types): Registry<Name, Types> {
return new Registry(name, { ...types });
}
static from<
Name extends string,
NewName extends string = Name,
const Types extends RegistryTypes = RegistryTypes,
>(other: Registry<Name, Types>, name?: NewName): Registry<NewName, Types> {
return new Registry(
(name ?? other.name) as NewName,
{ ...other.types },
);
}
constructor(
readonly name: Name,
readonly types: Types,
) {
this.types = { ...types };
}
register<K extends string, T extends Type<any>>(
key: K,
lazyType: (this: this, registry: this) => T,
): Registry<Name, Types & { [P in K]: T }>;
register<K extends string, T extends Type<any>>(
key: K,
type: T | Type<Type.Infer<T>>,
): Registry<Name, Types & { [P in K]: T }>;
register<K extends string, T extends Type<any>>(
key: K,
type: T | ((this: this, registry: this) => T),
): Registry<Name, Types & { [P in K]: T }>;
register<K extends string, T extends Type<any>>(
key: K,
type: T | ((this: this, registry: this) => T),
): Registry<Name, Types & { [P in K]: T }> {
this.define(key, type);
return this as any;
}
define<K extends string, T extends Type<any>>(
key: K,
lazyType: (this: this, registry: this) => T,
): asserts this is this & Registry<Name, Types & { [P in K]: T }>;
define<K extends string, T extends Type<any>>(
key: K,
type: T | Type<Type.Infer<T>>,
): asserts this is this & Registry<Name, Types & { [P in K]: T }>;
define<K extends string, T extends Type<any>>(
key: K,
type: T | ((this: this, registry: this) => T),
): asserts this is this & Registry<Name, Types & { [P in K]: T }>;
define<K extends string, T extends Type<any>>(
key: K,
type: T | ((this: this, registry: this) => T),
): asserts this is this & Registry<Name, Types & { [P in K]: T }> {
const k = key as K & keyof Types;
if (isFunction(type) && !(type instanceof Type)) {
this.types[k] = FunctionPrototypeCall(type, this, this) as never;
} else {
this.types[k] = type as never;
}
}
get<K extends string = string & keyof Types>(
key: K,
): K extends keyof Types ? Types[K] : never;
get<K extends keyof Types>(key: K): Types[K];
get(key: string): any {
const type = this.types[key];
if (!(type && isFunction(type))) {
throw new Error(`Type ${String(key)} not found in registry ${this.name}`);
}
return type;
}
}
export function registry<
Name extends string,
const Types extends RegistryTypes,
>(name: Name, types: Types): Registry<Name, Types> {
return new Registry(name, types);
}
export type InferTuple<T, F = T> = T extends readonly [] ? []
: T extends readonly [infer A, ...infer B]
? B extends readonly [] ? [Infer<A>]
: [Infer<A>, ...InferTuple<B, F>]
: F extends readonly unknown[] ? F
: [];
export type Infer<T, F = T> = T extends { readonly _typeof: infer U } ? U
: T extends readonly unknown[] ? InferTuple<T>
: T extends (this: infer This, ...args: infer Args) => infer Return
? (this: Infer<This>, ...args: InferTuple<Args>) => Infer<Return>
: T extends (...args: infer Args) => infer Return
? (...args: InferTuple<Args>) => Infer<Return>
: T extends Record<PropertyKey, any> ? { [P in keyof T]: Infer<T[P]> }
: T extends Map<infer K extends Type<any>, infer V extends Type<any>>
? Map<Infer<K>, Infer<V>>
: T extends Set<infer V extends Type<any>> ? Set<Infer<V>>
: T extends WeakMap<infer K extends Type<any>, infer V extends Type<any>>
? WeakMap<Infer<K, WeakKey>, Infer<V>>
: T extends WeakSet<infer V extends Type<any>> ? WeakSet<Infer<V, WeakKey>>
: T extends WeakRef<infer V extends Type<any>> ? WeakRef<Infer<V, WeakKey>>
: F;
export type { Infer as infer };
export declare namespace Type {
export { defaultInspectOptions, defaultRenderOptions };
// export {
// fn as function,
// _null as null,
// _undefined as undefined,
// _void as void,
// any,
// array,
// asyncFunction,
// asyncGenerator,
// asyncGeneratorFunction,
// asyncIterable,
// asyncIterableIterator,
// asyncIterator,
// bigint,
// boolean,
// date,
// error,
// generator,
// generatorFunction,
// iterable,
// iterableIterator,
// iterator,
// map,
// number,
// object,
// primitive,
// record,
// regExp,
// set,
// string,
// symbol,
// tuple,
// unknown,
// weakMap,
// weakRef,
// weakSet,
// };
export type { Infer, Infer as infer };
}
import t, { Type} from "./index.ts";
type t = typeof t;
const UserSchema = t.object({
name: t.string.min(2).max(100),
age: t.number.min(0).optional(),
email: t.string.email().optional(),
address: t.object({
street: t.string.min(3).max(64),
city: t.string.min(3).max(32),
zipCode: t.string.len(5),
}).optional(),
hobbies: t.array(t.string.min(3).max(32)).optional(),
});
type UserSchema = Type.infer<typeof UserSchema>;
const reg = t.registry("global", t);
// we have to create an alias here with an explicit type annotation, otherwise
// TypeScript will forbid .define()'s `asserts this is T` return type
const r1: typeof reg = reg;
r1.define("zipCode", t.string.len(5));
r1.define("address", (r) =>
t.object({
street: t.string.min(3).max(64),
city: t.string.min(3).max(32),
zipCode: r.types.zipCode,
}));
r1.define(
"hobby",
t.object({
name: t.string.min(3).max(32),
info: t.string.min(10).max(256),
paid: t.boolean.optional(),
}),
);
r1.define(
"user",
(r) =>
t.object({
name: t.string.min(3),
age: t.number.filter((v) => v >= 18 && v <= 120),
email: t.string.email(),
address: r.types.address.or(t.string),
hobbies: t.array(t.string.or(r.types.hobby)),
phone: t.string.len(10),
}),
);
export const user_t = r1.types["user"];
export type user_t = typeof user_t._typeof;
export const valid_user: user_t = {
name: "John Doe",
age: 30,
email: "[email protected]",
address: {
street: "123 Main St",
city: "Anytown",
zipCode: "12345",
},
hobbies: [
"reading",
{
name: "gaming",
info: "I love playing video games.",
paid: true,
},
],
phone: "1234567890",
} as user_t;
export const invalid_user = {
name: "J", // Error: name must be at least 3 characters long
age: 11, // Error: age must be between 18 and 120
email: "invalid-email", // Error: invalid email format
address: "123 Main St", // Error: address must be an object
} as const;
export type invalid_user = typeof invalid_user;
export const check_users = () => {
const valid = user_t.decode(valid_user);
if (valid.success) {
console.log("Valid user:", valid.value);
} else {
console.error("Invalid user:", valid.error);
}
const invalid = user_t.decode(invalid_user);
if (invalid.success) {
console.log("Valid user:", invalid.value);
} else {
console.error("Invalid user:", invalid.error);
}
};
export const config = t.object({
project: t.string,
version: t.string,
settings: t.object({
port: t.number,
useSSL: t.boolean,
allowedOrigins: t.array(t.string),
}),
plugins: t.array(
t.object({
name: t.string,
enabled: t.boolean,
// For plugin options, keys are property keys and values are unknown.
options: t.record(t.string.or(t.symbol), t.unknown),
// Alternatively, you can use t.object() if you know the structure
// of the options object.
schema: t.object({
// Define the schema for the plugin options here.
name: t.string.match("^[a-zA-Z0-9_-]{3,}$"),
}),
}),
),
});
export type config = t.infer<typeof config>;
export function demoValidation(input: unknown) {
const result = config.decode(input);
if (result.success) {
console.log("Validation succeeded. Config:", result.value);
} else {
let err = "Validation failed with errors:";
for (
const error of result.errors ?? [t.ValidationError.from(result.error)]
) {
err += ` [${
error.path?.length ? error.path.join(".") : "<root>"
}]\n message = "${error.message}"\n cause = ${JSON.stringify(error.cause)}\n stack = ${JSON.stringify(error.stack)}\n`;
}
throw new TypeError(err);
}
}
// Example valid configuration object.
export const validConfig = {
project: "MyProject",
version: "1.0.0",
settings: {
port: 8080,
useSSL: true,
allowedOrigins: ["http://localhost", "https://example.com"],
},
plugins: [
{
name: "pluginA",
enabled: true,
options: { optionA: "valueA", optionB: 123 },
},
{
name: "pluginB",
enabled: false,
options: {},
},
],
};
export type validConfig = typeof validConfig;
// Example invalid configuration object.
export const invalidConfig = {
project: 123, // Error: should be a string.
version: "1.0.0",
settings: {
port: "not a number", // Error: should be a number.
useSSL: "yes", // Error: should be a boolean.
allowedOrigins: [true, false], // Error: should be an array of strings.
},
plugins: [
{
name: "pluginA",
enabled: "sometimes", // Error: should be a boolean.
options: { optionA: "valueA" },
},
],
} as const;
export type invalidConfig = typeof invalidConfig;
if (import.meta.main) {
console.log("=== Valid User Test ===");
check_users();
console.log("\n=== Valid Config Test ===");
try {
demoValidation(validConfig);
console.log("Heck yeah — 'validConfig' successfully PASSED runtime type checks!!!");
} catch (e) {
console.error("Aww, motherfungus! ... 'validConfig' FAILED runtime type checks:");
console.error(e);
}
try {
demoValidation(invalidConfig);
console.error("Uh oh, 'invalidConfig' PASSED runtime type checks. It should have FAILED!!!");
console.error(invalidConfig);
} catch (e) {
console.log("Aaaand — as expected — 'invalidConfig' failed runtime type checks. Atta boy, Fido!");
console.log(e);
}
console.log("\n=== User Schema Direct Test ===");
const userResult = UserSchema.decode(valid_user);
if (userResult.success) {
console.log("Valid user:", userResult.value);
} else {
console.error("Invalid user:", userResult.error);
}
console.log("\n --- fin --- \n")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment