ascertain
Version:
0-Deps, simple, fast, for browser and node js object schema validator
554 lines (553 loc) • 19.5 kB
TypeScript
/**
* Symbol for validating object keys against a schema.
*/
export declare const $keys: unique symbol;
/**
* Symbol for validating object values against a schema.
*/
export declare const $values: unique symbol;
/**
* Symbol for enforcing strict object validation (no extra properties allowed).
*/
export declare const $strict: unique symbol;
declare const $op: unique symbol;
declare const OR: unique symbol;
declare const AND: unique symbol;
declare const OPTIONAL: unique symbol;
declare const TUPLE: unique symbol;
declare const DISCRIMINATED: unique symbol;
declare const CHECK: unique symbol;
interface OrShape<T> {
readonly schemas: Schema<T>[];
readonly [$op]: typeof OR;
}
interface AndShape<T> {
readonly schemas: Schema<T>[];
readonly [$op]: typeof AND;
}
interface OptionalShape<T> {
readonly schemas: Schema<T>[];
readonly [$op]: typeof OPTIONAL;
}
interface TupleShape<T> {
readonly schemas: Schema<T>[];
readonly [$op]: typeof TUPLE;
}
interface DiscriminatedShape<T> {
readonly schemas: Schema<T>[];
readonly [$op]: typeof DISCRIMINATED;
readonly key: string;
}
export interface CheckContext {
ref(value: unknown): string;
}
interface CheckShape {
readonly [$op]: typeof CHECK;
readonly compile: (value: string, ctx: CheckContext) => {
check: string;
message: string;
};
}
/**
* Represents a schema for validating data.
*
* Schemas can be defined for various data types, including objects, arrays, and primitives.
*
* @template T - The type of data the schema validates.
*/
export type Schema<T> = T extends Record<string | number | symbol, unknown> ? {
[K in keyof T]?: Schema<T[K]> | unknown;
} & {
[$keys]?: Schema<keyof T>;
} & {
[$values]?: Schema<T[keyof T]>;
} & {
[$strict]?: boolean;
} : T extends Array<infer A> ? Schema<A>[] | unknown : unknown;
/**
* Operator for validating data against any of the provided schemas (logical OR).
*/
export declare const or: <T>(...schemas: Schema<T>[]) => OrShape<T>;
/**
* Operator for validating data against all provided schemas (logical AND).
*/
export declare const and: <T>(...schemas: Schema<T>[]) => AndShape<T>;
/**
* Operator for making a schema optional (nullable).
*/
export declare const optional: <T>(schema: Schema<T>) => OptionalShape<T>;
/**
* Operator for validating data against a fixed-length tuple of schemas.
*/
export declare const tuple: <T>(...schemas: Schema<T>[]) => TupleShape<T>;
/**
* Operator for validating data against a discriminated union.
*
* Optimizes validation by checking the discriminant field first and only
* validating the matching variant. More efficient than `or()` for unions
* where each variant has a common field with a unique literal value.
*
* @param schemas - Array of object schemas, each with a discriminant field containing a literal value.
* @param key - The name of the discriminant field present in all variants.
*
* @example
* ```typescript
* const messageSchema = discriminated([
* { type: 'email', address: String },
* { type: 'sms', phone: String },
* { type: 'push', token: String },
* ], 'type');
* ```
*/
export declare const discriminated: <T>(schemas: Schema<T>[], key: string) => DiscriminatedShape<T>;
/**
* Creates a custom validation check.
* Accepts a predicate function or an object with a compile method for inlined checks.
*
* @param fnOrOpts - A predicate function `(value) => boolean` or an object with a `compile` method for code-generating checks.
* @param message - Optional custom error message.
*/
export declare const check: (fnOrOpts: ((v: unknown) => boolean) | {
compile: (value: string, ctx: CheckContext) => {
check: string;
message: string;
};
}, message?: string) => CheckShape;
/**
* Validates that a numeric value is greater than or equal to `n`.
*
* @param n - The minimum allowed value (inclusive).
* @param message - Optional custom error message.
*/
export declare const min: (n: number, message?: string) => CheckShape;
/**
* Validates that a numeric value is less than or equal to `n`.
*
* @param n - The maximum allowed value (inclusive).
* @param message - Optional custom error message.
*/
export declare const max: (n: number, message?: string) => CheckShape;
/**
* Validates that a value is an integer.
*
* @param message - Optional custom error message.
*/
export declare const integer: (message?: string) => CheckShape;
/**
* Validates that a value's length is greater than or equal to `n`.
*
* @param n - The minimum allowed length (inclusive).
* @param message - Optional custom error message.
*/
export declare const minLength: (n: number, message?: string) => CheckShape;
/**
* Validates that a value's length is less than or equal to `n`.
*
* @param n - The maximum allowed length (inclusive).
* @param message - Optional custom error message.
*/
export declare const maxLength: (n: number, message?: string) => CheckShape;
/**
* Validates that a numeric value is strictly greater than `n`.
*
* @param n - The exclusive lower bound.
* @param message - Optional custom error message.
*/
export declare const gt: (n: number, message?: string) => CheckShape;
/**
* Validates that a numeric value is strictly less than `n`.
*
* @param n - The exclusive upper bound.
* @param message - Optional custom error message.
*/
export declare const lt: (n: number, message?: string) => CheckShape;
/**
* Validates that a numeric value is a multiple of `n`.
*
* @param n - The divisor to check against.
* @param message - Optional custom error message.
*/
export declare const multipleOf: (n: number, message?: string) => CheckShape;
/**
* Validates that an array contains only unique items.
*
* @param message - Optional custom error message.
*/
export declare const uniqueItems: (message?: string) => CheckShape;
type EnumLike = {
[k: string]: string | number;
[n: number]: string;
};
/**
* Validates that a value is one of the allowed values. Accepts an array or an enum-like object.
*
* @param values - Array of allowed values or an enum-like object.
* @param message - Optional custom error message.
*/
export declare const oneOf: <T extends EnumLike>(values: (string | number)[] | T, message?: string) => CheckShape;
/**
* Decodes a base64-encoded string to UTF-8.
*
* Uses `Buffer` in Node.js environments and `atob` in browsers.
*
* @param value - The base64-encoded string to decode.
* @returns The decoded UTF-8 string.
*/
export declare const fromBase64: (value: string) => string;
/**
* Creates a TypeError with the given message, typed as T for deferred error handling.
*
* Used by `as.*` conversion utilities to return errors that can be caught
* during schema validation rather than throwing immediately.
*
* @template T - The expected return type (for type compatibility with conversion functions).
* @param message - The error message.
* @returns A TypeError instance typed as T.
*/
export declare const asError: <T>(message: string) => T;
/**
* Type casting utilities for parsing strings into typed values.
* Useful for environment variables, query parameters, and other string inputs.
* Returns a TypeError for invalid values, enabling deferred validation with `ascertain()`.
*/
export declare const as: {
/**
* Attempts to convert a value to a string.
*
* @param value - The value to convert.
* @returns The value as a string, or a TypeError if not a string.
*/
string: (value: string | undefined) => string;
/**
* Attempts to convert a value to a number.
*
* Supports integers, floats, scientific notation (1e10), and prefixed formats:
* - Hexadecimal: `0x` or `0X` (e.g., `'0xFF'` → 255)
* - Octal: `0o` or `0O` (e.g., `'0o77'` → 63)
* - Binary: `0b` or `0B` (e.g., `'0b1010'` → 10)
*
* All formats support optional leading sign (`+` or `-`).
*
* @param value - The value to convert (expected to be a string representation of a number).
* @returns The value as a number, or a TypeError if not a valid number.
*/
number: (value: string | undefined) => number;
/**
* Attempts to convert a value to a Date object.
*
* @param value - The value to convert (expected to be a string representation of a date).
* @returns The value as a Date object, or a TypeError if not a valid date.
*/
date: (value: string | undefined) => Date;
/**
* Attempts to convert a value to a time duration in milliseconds.
*
* @param value - The value to convert (e.g., "5s" for 5 seconds).
* @param conversionFactor - Optional factor to divide the result by (default is 1).
* @returns The time duration in milliseconds, or a TypeError if the format is invalid.
*/
time: (value: string | undefined, conversionFactor?: number) => number;
/**
* Attempts to convert a value to a boolean.
*
* @param value - The boolean like value to convert (e.g., "true", "1", "enabled").
* @returns The value as a boolean, or a TypeError if it could not be converted to a boolean.
*/
boolean: (value: string | undefined) => boolean;
/**
* Attempts to convert a string into an array of strings by splitting it using the given delimiter.
*
* @param value - The string value to attempt to split into an array.
* @param delimiter - The character or string used to separate elements in the input string.
* @returns An array of strings if the conversion is successful, or a TypeError if the value is not a string.
*/
array: (value: string | undefined, delimiter: string) => string[];
/**
* Attempts to parse a JSON string into a JavaScript object.
*
* @template T - The expected type of the parsed JSON object.
* @param value - The JSON string to attempt to parse.
* @returns The parsed JSON object if successful, or a TypeError if the value is not valid JSON.
*/
json: <T = object>(value: string | undefined) => T;
/**
* Attempts to decode a base64-encoded string.
*
* @param value - The base64-encoded string to attempt to decode.
* @returns The decoded string if successful, or a TypeError if the value is not valid base64.
*/
base64: (value: string | undefined) => string;
};
/**
* String format validators for common patterns (RFC 3339 date-time, email, URI, UUID, etc.).
* Each method returns a CheckShape that can be composed with `and()` for schema validation.
*/
export declare const format: {
dateTime: (message?: string) => CheckShape;
date: (message?: string) => CheckShape;
time: (message?: string) => CheckShape;
duration: (message?: string) => CheckShape;
email: (message?: string) => CheckShape;
idnEmail: (message?: string) => CheckShape;
hostname: (message?: string) => CheckShape;
idnHostname: (message?: string) => CheckShape;
ipv4: (message?: string) => CheckShape;
ipv6: (message?: string) => CheckShape;
uri: (message?: string) => CheckShape;
uriReference: (message?: string) => CheckShape;
iri: (message?: string) => CheckShape;
iriReference: (message?: string) => CheckShape;
uuid: (message?: string) => CheckShape;
uriTemplate: (message?: string) => CheckShape;
jsonPointer: (message?: string) => CheckShape;
relativeJsonPointer: (message?: string) => CheckShape;
regex: (message?: string) => CheckShape;
};
/**
* Validator function returned by compile().
* Returns true if valid, false if invalid.
* Access `.issues` property after validation to get error details.
*/
export interface Validator<T> {
(data: T): boolean;
issues: ReadonlyArray<StandardSchemaV1.Issue>;
}
/**
* Options for the compile function.
*/
export interface CompileOptions {
/**
* When true, collects all validation errors instead of stopping at the first.
* Default is false (first-error mode) for optimal performance.
*/
allErrors?: boolean;
}
/**
* Compiles a schema into a high-performance validation function.
*
* By default uses first-error mode which stops at the first validation failure
* and returns immediately. This provides optimal performance for invalid data.
*
* Set `allErrors: true` to collect all validation errors (slower but more informative).
*
* @template T - The type of data the schema validates.
* @param schema - The schema to compile.
* @param options - Optional configuration (allErrors: boolean).
* @returns A validator function that returns boolean. Access `.issues` for error details.
*
* @example
* ```typescript
* import { compile, optional, or } from 'ascertain';
*
* const userSchema = {
* name: String,
* age: Number,
* email: optional(String),
* role: or('admin', 'user', 'guest')
* };
*
* // First-error mode (default) - fastest for invalid data
* const validate = compile(userSchema);
*
* // All-errors mode - collects all validation issues
* const validateAll = compile(userSchema, { allErrors: true });
*
* // Valid data
* if (validate({ name: 'John', age: 30, role: 'user' })) {
* console.log('Valid!');
* }
*
* // Invalid data - check .issues for details
* if (!validate({ name: 123, age: 'thirty' })) {
* console.log(validate.issues); // Array with first validation issue
* }
* ```
*/
export declare const compile: <T>(schema: Schema<T>, options?: CompileOptions) => Validator<T>;
/**
* Asserts that data conforms to a given schema.
*
* This function is a convenient wrapper around `compile`. It compiles the schema
* and immediately validates the provided data against it.
*
* @template T - The type of data the schema validates.
* @param schema - The schema to validate against.
* @param data - The data to validate.
* @throws `{TypeError}` If the data does not conform to the schema.
*
* @example
* ```typescript
* import { ascertain, optional, and, or } from 'ascertain';
*
* const userSchema = {
* name: String,
* age: Number,
* email: optional(String),
* active: Boolean
* };
*
* const userData = {
* name: 'Alice',
* age: 25,
* email: 'alice@example.com',
* active: true
* };
*
* // Validate data - throws if invalid, otherwise continues silently
* ascertain(userSchema, userData);
* console.log('User data is valid!');
*
* // Example with invalid data
* try {
* ascertain(userSchema, {
* name: 'Bob',
* age: 'twenty-five', // Invalid: should be number
* active: true
* });
* } catch (error) {
* console.error('Validation failed:', error.message);
* }
* ```
*
* @example
* ```typescript
* // Array validation
* const numbersSchema = [Number];
* const numbers = [1, 2, 3, 4, 5];
*
* ascertain(numbersSchema, numbers);
*
* // Tuple validation
* const coordinateSchema = tuple(Number, Number);
* const point = [10, 20];
*
* ascertain(coordinateSchema, point);
* ```
*/
export declare const ascertain: <T>(schema: Schema<T>, data: T) => void;
/**
* Extracts the shape of a config object based on the schema keys.
* Recursively picks only the properties defined in the schema.
*/
export type ExtractShape<C, S> = {
[K in keyof S & keyof C]: S[K] extends object ? (C[K] extends object ? ExtractShape<C[K], S[K]> : C[K]) : C[K];
};
/**
* Creates a typed validator function for a config object.
*
* Returns a function that validates a schema against the config and returns
* the same config reference with a narrowed type containing only the validated fields.
*
* @template C - The type of the config object.
* @param config - The config object to validate against.
* @returns A validator function that takes a schema and returns the typed config subset.
*
* @example
* ```typescript
* import { createValidator, as } from 'ascertain';
*
* const config = {
* app: { name: as.string(process.env.APP_NAME) },
* kafka: { brokers: as.array(process.env.BROKERS, ',') },
* redis: { host: as.string(process.env.REDIS_HOST) },
* };
*
* const validate = createValidator(config);
*
* // Consumer only validates what it needs
* const { app, kafka } = validate({
* app: { name: String },
* kafka: { brokers: [String] },
* });
*
* // app.name is typed as string
* // kafka.brokers is typed as string[]
* // redis is not accessible - TypeScript error
* ```
*/
export declare const createValidator: <C>(config: C) => <S extends Schema<Partial<C>>>(schema: S) => ExtractShape<C, S>;
export interface StandardSchemaV1<Input = unknown, Output = Input> {
readonly '~standard': StandardSchemaV1.Props<Input, Output>;
}
export declare namespace StandardSchemaV1 {
interface Props<Input = unknown, Output = Input> {
readonly version: 1;
readonly vendor: string;
readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
readonly types?: {
readonly input: Input;
readonly output: Output;
};
}
type Result<Output> = SuccessResult<Output> | FailureResult;
interface SuccessResult<Output> {
readonly value: Output;
readonly issues?: undefined;
}
interface FailureResult {
readonly issues: ReadonlyArray<Issue>;
}
interface Issue {
readonly message: string;
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
}
interface PathSegment {
readonly key: PropertyKey;
}
}
/**
* Wraps an Ascertain schema to make it Standard Schema v1 compliant.
*
* Creates a validator that implements the Standard Schema specification,
* enabling interoperability with tools like tRPC, TanStack Form, and other
* ecosystem libraries that consume Standard Schema-compliant validators.
*
* The returned function can be used both as a regular Ascertain validator
* (throws on error) and as a Standard Schema validator (returns result object).
*
* @template T - The type of data the schema validates.
* @param schema - The Ascertain schema to wrap.
* @returns A function that validates data, with a `~standard` property for Standard Schema compliance.
*
* @see https://standardschema.dev/
*
* @example
* ```typescript
* import { standardSchema, or, optional } from 'ascertain';
*
* // Create a Standard Schema-compliant validator
* const userValidator = standardSchema({
* name: String,
* age: Number,
* role: or('admin', 'user'),
* email: optional(String),
* });
*
* // Use as regular Ascertain validator (throws on error)
* userValidator({ name: 'Alice', age: 30, role: 'admin' });
*
* // Use Standard Schema interface (returns result object)
* const result = userValidator['~standard'].validate(unknownData);
* if (result.issues) {
* console.log(result.issues);
* } else {
* console.log(result.value); // typed as User
* }
*
* // Works with tRPC, TanStack Form, etc.
* ```
*/
interface StandardSchemaFn<T> {
(data: T): void;
'~standard': StandardSchemaV1.Props<T, T>;
}
/**
* Wraps a schema for Standard Schema v1 compliance.
* The returned function throws on invalid data and exposes a `~standard` interface
* for interoperability with tRPC, TanStack Form, and other ecosystem tools.
*
* @param schema - The schema to wrap.
* @returns A validator function with a `~standard` property conforming to Standard Schema v1.
*/
export declare const standardSchema: <T>(schema: Schema<T>) => StandardSchemaFn<T>;
export {};