Keyborg/shared/utils/validator.ts

1208 lines
38 KiB
TypeScript

import { err, Result } from "@shared/utils/result.ts";
import { ok } from "@shared/utils/index.ts";
import { None, none, Option, some } from "@shared/utils/option.ts";
// ── Error Types ─────────────────────────────────────────────────────
type ValidationErrorDetail =
| {
kind: "typeMismatch";
expected: string;
received: string;
msg?: string;
}
| {
kind: "propertyValidation";
property: string;
detail: ValidationErrorDetail;
msg?: string;
}
| {
kind: "missingProperties" | "unexpectedProperties";
keys: string[];
msg?: string;
}
| {
kind: "unionValidation";
details: ValidationErrorDetail[];
msg?: string;
}
| {
kind: "arrayElement";
index: number;
detail: ValidationErrorDetail;
msg?: string;
}
| { kind: "general"; mark?: string; msg: string };
class SchemaValidationError extends Error {
public readonly type = "SchemaValidationError";
constructor(
public readonly input: unknown,
public readonly detail: ValidationErrorDetail,
) {
super(detail.msg || "Schema validation error");
}
public format(): Record<string, unknown> {
return {
input: this.input,
error: SchemaValidationError.formatDetail(this.detail),
};
}
get msg(): string {
return SchemaValidationError.getBestMsg(this.detail);
}
private static getBestMsg(detail: ValidationErrorDetail): string {
switch (detail.kind) {
case "typeMismatch":
case "unexpectedProperties":
case "missingProperties":
case "general":
case "unionValidation":
return SchemaValidationError.formatMsg(detail);
case "propertyValidation":
case "arrayElement":
return detail.msg ||
SchemaValidationError.getBestMsg(detail.detail);
default:
return "Unknown error";
}
}
private static formatDetail(detail: ValidationErrorDetail): any {
switch (detail.kind) {
case "general":
case "typeMismatch":
return SchemaValidationError.formatMsg(detail);
case "propertyValidation":
return {
[detail.property]: detail.msg ||
this.formatDetail(detail.detail!),
};
case "unexpectedProperties":
case "missingProperties": {
const msg = detail.msg ||
(detail.kind === "unexpectedProperties"
? "Property is not allowed in a strict schema object"
: "Property is required, but missing");
return detail.keys.reduce<Record<string, string>>(
(acc, key) => {
acc[key] = msg;
return acc;
},
{},
);
}
case "arrayElement": {
const detailObj: Record<string, string> = {};
if (detail.msg) {
detailObj["msg"] = detail.msg;
}
detailObj[`index_${detail.index}`] = this.formatDetail(
detail.detail,
);
return detailObj;
}
case "unionValidation": {
const arr: unknown[] = detail.details?.map(
(err): unknown => this.formatDetail(err),
);
if (detail.msg) {
arr.unshift("Msg: " + detail.msg);
}
return arr;
}
default:
return "Unknown error type";
}
}
private static formatMsg(detail: ValidationErrorDetail): string {
if (detail.msg || detail.kind === "general") {
return detail.msg || "Unknown error";
}
switch (detail.kind) {
case "typeMismatch":
return `Expected ${detail.expected}, but received ${detail.received}`;
case "unexpectedProperties":
return `Properties not allowed: ${detail.keys.join(", ")}`;
case "missingProperties":
return `Missing required properties: ${detail.keys.join(", ")}`;
case "unionValidation":
return `Input did not match any union member`;
default:
return "Unknown error";
}
}
}
function createValidationError(
input: unknown,
error: ValidationErrorDetail,
) {
return new SchemaValidationError(input, error);
}
type PrimitiveTypeName =
| "string"
| "number"
| "bigint"
| "boolean"
| "symbol"
| "undefined"
| "object"
| "function";
// ── Core Schema Types ───────────────────────────────────────────────
export interface Schema<T> {
parse(input: unknown): Result<T, SchemaValidationError>;
}
type ValidationCheck<T> = (value: T) => SchemaValidationError | void;
export abstract class BaseSchema<T> implements Schema<T> {
protected checks: ValidationCheck<T>[] = [];
constructor(public readonly msg?: string) {}
public parse(input: unknown): Result<T, SchemaValidationError> {
return this.validateInput(input).andThen((value) =>
this.applyValidationChecks(value)
);
}
protected abstract validateInput(
input: unknown,
): Result<T, SchemaValidationError>;
protected static validatePrimitive<U>(
input: unknown,
expectedType: PrimitiveTypeName,
msg?: string,
): Result<U, SchemaValidationError> {
const receivedType = typeof input;
return receivedType === expectedType ? ok(input as U) : err(
createValidationError(input, {
kind: "typeMismatch",
expected: expectedType,
received: receivedType,
msg: msg,
}),
);
}
public addCheck(check: ValidationCheck<T>): this {
this.checks.push(check);
return this;
}
protected applyValidationChecks(
value: T,
): Result<T, SchemaValidationError> {
for (const check of this.checks) {
const error = check(value);
if (error) {
return err(error);
}
}
return ok(value);
}
protected static isNullishSchema(schema: Schema<any>): boolean {
return schema.parse(null).isOk() || schema.parse(undefined).isOk();
}
}
class StringSchema extends BaseSchema<string> {
private static readonly emailRegex =
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; // https://stackoverflow.com/questions/46155/how-can-i-validate-an-email-address-in-javascript
private static readonly ipRegex =
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; // https://stackoverflow.com/questions/4460586/javascript-regular-expression-to-check-for-ip-addresses
protected override validateInput(
input: unknown,
): Result<string, SchemaValidationError> {
return BaseSchema.validatePrimitive<string>(
input,
"string",
this.msg,
);
}
public max(len: number, msg?: string): this {
return this.addCheck((value) => {
if (value.length > len) {
return createValidationError(value, {
kind: "general",
msg: msg || `String must be at most ${len} characters long`,
});
}
});
}
public min(len: number, msg?: string): this {
return this.addCheck((value) => {
if (value.length < len) {
return createValidationError(value, {
kind: "general",
msg: msg || `String must be at most ${len} characters long`,
});
}
});
}
public regex(pattern: RegExp, msg?: string): this {
return this.addCheck((value) => {
if (!pattern.test(value)) {
return createValidationError(value, {
kind: "general",
msg: msg || `String must match pattern ${String(pattern)}`,
});
}
});
}
public email(msg?: string): this {
return this.regex(
StringSchema.emailRegex,
msg || "String must be a valid email address",
);
}
public ip(msg?: string): this {
return this.regex(
StringSchema.ipRegex,
msg || "String must be a valid ip address",
);
}
}
class LiteralSchema<L extends string> extends BaseSchema<L> {
constructor(
public readonly literal: L,
msg?: string,
) {
super(msg);
}
protected override validateInput(
input: unknown,
): Result<L, SchemaValidationError> {
return BaseSchema.validatePrimitive<string>(
input,
"string",
this.msg,
)
.andThen((str) =>
str === this.literal ? ok(str as L) : err(
createValidationError(
input,
{
kind: "typeMismatch",
expected: this.literal,
received: str,
msg: this.msg,
},
),
)
);
}
}
class NumberSchema extends BaseSchema<number> {
protected override validateInput(
input: unknown,
): Result<number, SchemaValidationError> {
return BaseSchema.validatePrimitive<number>(
input,
"number",
this.msg,
);
}
public gt(
num: number,
msg?: string,
): this {
return this.addCheck((value) => {
if (value <= num) {
return createValidationError(
value,
{
kind: "general",
msg: msg || `Number must be greater than ${num}`,
},
);
}
});
}
public gte(
num: number,
msg?: string,
): this {
return this.addCheck((value) => {
if (value < num) {
return createValidationError(
value,
{
kind: "general",
msg: msg ||
`Number must be greater than or equal to ${num}`,
},
);
}
});
}
public lt(
num: number,
msg?: string,
): this {
return this.addCheck((value) => {
if (value >= num) {
return createValidationError(
value,
{
kind: "general",
msg: msg || `Number must be less than ${num}`,
},
);
}
});
}
public lte(
num: number,
msg?: string,
): this {
return this.addCheck((value) => {
if (value > num) {
return createValidationError(
value,
{
kind: "general",
msg: msg ||
`Number must be less than or equal to ${num}`,
},
);
}
});
}
public int(
msg?: string,
): this {
return this.addCheck((value) => {
if (!Number.isInteger(value)) {
return createValidationError(
value,
{
kind: "general",
msg: msg ||
`Number must be an integer`,
},
);
}
});
}
public positive(
msg?: string,
): this {
return this.gt(0, msg || "Number must be positive");
}
public nonnegative(
msg?: string,
) {
return this.gte(0, msg || "Number must be nonnegative");
}
public negative(
msg?: string,
): this {
return this.lt(0, msg || "Number must be negative");
}
public nonpositive(
msg?: string,
) {
return this.lte(0, msg || "Number must be nonpositive");
}
public finite(
msg?: string,
): this {
return this.addCheck((value) => {
if (!Number.isFinite(value)) {
return createValidationError(
value,
{
kind: "general",
msg: msg ||
`Number must be an integer`,
},
);
}
});
}
public safe(
msg?: string,
): this {
return this.addCheck((value) => {
if (!Number.isSafeInteger(value)) {
return createValidationError(
value,
{
kind: "general",
msg: msg ||
`Number must be an integer`,
},
);
}
});
}
public multipleOf(
num: number,
msg?: string,
): this {
return this.addCheck((value) => {
if (value % num !== 0) {
return createValidationError(
value,
{
kind: "general",
msg: msg || `Number must be a multiple of ${num}`,
},
);
}
});
}
}
class BigintSchema extends BaseSchema<bigint> {
protected override validateInput(
input: unknown,
): Result<bigint, SchemaValidationError> {
return BaseSchema.validatePrimitive<bigint>(
input,
"bigint",
this.msg,
);
}
}
class BooleanSchema extends BaseSchema<boolean> {
protected override validateInput(
input: unknown,
): Result<boolean, SchemaValidationError> {
return BaseSchema.validatePrimitive<boolean>(
input,
"boolean",
this.msg,
);
}
}
class DateSchema extends BaseSchema<Date> {
protected override validateInput(
input: unknown,
): Result<Date, SchemaValidationError> {
return BaseSchema.validatePrimitive<object>(
input,
"object",
this.msg,
)
.andThen((o) => {
if (o instanceof Date) return ok(o as Date);
const received = o?.constructor?.name ?? "unknown";
return err(createValidationError(
input,
{
kind: "typeMismatch",
expected: "Date instance",
received,
msg: this.msg,
},
));
});
}
}
class SymbolSchema extends BaseSchema<symbol> {
protected override validateInput(
input: unknown,
): Result<symbol, SchemaValidationError> {
return BaseSchema.validatePrimitive<symbol>(
input,
"symbol",
this.msg,
);
}
}
class UndefinedSchema extends BaseSchema<undefined> {
protected override validateInput(
input: unknown,
): Result<undefined, SchemaValidationError> {
return BaseSchema.validatePrimitive<undefined>(
input,
"undefined",
this.msg,
);
}
}
class NullSchema extends BaseSchema<null> {
protected override validateInput(
input: unknown,
): Result<null, SchemaValidationError> {
if (input === null) return ok(input);
const received = typeof input === "object"
? input?.constructor?.name ?? "unknown"
: typeof input;
return err(
createValidationError(
input,
{
kind: "typeMismatch",
expected: "null",
received,
msg: this.msg,
},
),
);
}
}
class VoidSchema extends BaseSchema<void> {
protected override validateInput(
input: unknown,
): Result<void, SchemaValidationError> {
if (input === undefined || input === null) return ok();
const received = typeof input === "object"
? input?.constructor?.name ?? "unknown"
: typeof input;
return err(createValidationError(
input,
{
kind: "typeMismatch",
expected: "void (undefined/null)",
received,
msg: this.msg,
},
));
}
}
class AnySchema extends BaseSchema<any> {
protected override validateInput(
input: unknown,
): Result<any, SchemaValidationError> {
return ok(input);
}
}
class UnknownSchema extends BaseSchema<unknown> {
protected override validateInput(
input: unknown,
): Result<unknown, SchemaValidationError> {
return ok(input);
}
}
class NeverSchema extends BaseSchema<never> {
protected override validateInput(
input: unknown,
): Result<never, SchemaValidationError> {
return err(
createValidationError(
input,
{
kind: "typeMismatch",
expected: "never",
received: typeof input,
msg: this.msg,
},
),
);
}
}
class ObjectSchema<S extends Record<string, Schema<any>>>
extends BaseSchema<{ [K in keyof S]: InferSchemaType<S[K]> }> {
private strictMode: boolean = false;
private objectMsg?;
constructor(
public readonly shape: S,
msg?: {
mismatch?: string;
nullObject?: string;
unexpectedProperty?: string;
propertyValidation?: string;
missingProperty?: string;
} | string,
) {
let mismatchMsg: string | undefined;
let objectMsg;
if (typeof msg === "string") {
mismatchMsg = msg;
} else if (typeof msg === "object") {
objectMsg = msg;
}
super(mismatchMsg);
this.objectMsg = objectMsg;
}
// TODO: Simplify it a bit
protected override validateInput(
input: unknown,
): Result<
{ [K in keyof S]: InferSchemaType<S[K]> },
SchemaValidationError
> {
return BaseSchema.validatePrimitive<object>(
input,
"object",
this.msg || this.objectMsg?.mismatch,
)
.andThen((obj) => {
if (obj === null) {
return err(
createValidationError(input, {
kind: "typeMismatch",
expected: "Non-null object",
received: "null",
msg: this.msg || this.objectMsg?.nullObject,
}),
);
}
const resultObj: Record<string, unknown> = {};
const expectedKeys = new Set(Object.keys(this.shape));
for (const key of Object.keys(obj)) {
const schema = this.shape[key];
if (schema === undefined) {
if (this.strictMode) {
const keys = new Set(Object.keys(obj)).difference(
new Set(Object.keys(this.shape)),
).keys().toArray();
return err(
createValidationError(
input,
{
kind: "unexpectedProperties",
keys,
msg: this.msg ||
this.objectMsg
?.unexpectedProperty,
},
),
);
}
continue;
}
const result = schema.parse((obj as any)[key]);
if (result.isErr()) {
return err(
createValidationError(
input,
{
kind: "propertyValidation",
property: key,
detail: result.error.detail,
msg: this.msg ||
this.objectMsg?.propertyValidation,
},
),
);
}
expectedKeys.delete(key);
resultObj[key] = result.value;
}
const missingProperties = expectedKeys.keys().filter(
(key) => !BaseSchema.isNullishSchema(this.shape[key]),
).toArray();
if (missingProperties.length > 0) {
return err(
createValidationError(
input,
{
kind: "missingProperties",
keys: missingProperties,
msg: this.msg ||
this.objectMsg?.missingProperty,
},
),
);
}
return ok(
resultObj as { [K in keyof S]: InferSchemaType<S[K]> },
);
});
}
strict(): this {
this.strictMode = true;
return this;
}
}
type InferUnionSchemaType<U extends Schema<any>[]> = U[number] extends
Schema<infer T> ? T : never;
class UnionSchema<U extends Schema<any>[]>
extends BaseSchema<InferUnionSchemaType<U>> {
private unionMsg?;
constructor(
public readonly schemas: U,
msg?: {
mismatch?: string;
unionValidation?: string;
} | string,
) {
let mismatchMsg: string | undefined;
let unionMsg;
if (typeof msg === "string") {
mismatchMsg = msg;
} else if (typeof msg === "object") {
unionMsg = msg;
}
super(mismatchMsg);
this.unionMsg = unionMsg;
}
private static getTypeFromSchemaName(name: string): string {
switch (name) {
case "StringSchema":
case "LiteralSchema":
return "string";
case "NumberSchema":
return "number";
case "BigintSchema":
return "bigint";
case "BooleanSchema":
return "boolean";
case "UndefinedSchema":
return "undefined";
case "SymbolSchema":
return "symbol";
default:
return "object";
}
}
protected validateInput(
input: unknown,
): Result<InferUnionSchemaType<U>, SchemaValidationError> {
const errors: ValidationErrorDetail[] = [];
let allTypeMismatch = true;
for (const schema of this.schemas) {
const result = schema.parse(input);
if (result.isOk()) {
return ok(result.value as InferUnionSchemaType<U>);
}
allTypeMismatch = result.error.detail?.kind === "typeMismatch" &&
allTypeMismatch;
errors.push(result.error.detail);
}
if (allTypeMismatch) {
return err(createValidationError(input, {
kind: "typeMismatch",
expected: this.schemas.map((s) =>
UnionSchema.getTypeFromSchemaName(
s.constructor.name,
)
).join(" | "),
received: typeof input,
msg: this.msg || this.unionMsg?.mismatch,
}));
}
return err(
createValidationError(input, {
kind: "unionValidation",
msg: this.msg ||
this.unionMsg?.unionValidation ||
"Input did not match any union member",
details: errors,
}),
);
}
}
class ArraySchema<S extends Schema<any>>
extends BaseSchema<InferSchemaType<S>[]> {
private arrayMsg?;
constructor(
private readonly schema: S,
msg?: {
mismatch?: string;
element?: string;
} | string,
) {
let mismatchMsg: string | undefined;
let arrayMsg;
if (typeof msg === "string") {
mismatchMsg = msg;
} else if (typeof msg === "object") {
arrayMsg = msg;
}
super(mismatchMsg);
// TODO: abstract complex schemas in a separate type with thos messages
this.arrayMsg = arrayMsg;
}
protected override validateInput(
input: unknown,
): Result<InferSchemaType<S>[], SchemaValidationError> {
if (!Array.isArray(input)) {
return err(createValidationError(input, {
kind: "typeMismatch",
expected: "Array",
received: "Non-array",
msg: this.msg || this.arrayMsg?.mismatch,
}));
}
for (let i = 0; i < input.length; i++) {
const result = this.schema.parse(input[i]);
if (result.isErr()) {
return err(
createValidationError(input, {
kind: "arrayElement",
index: i,
detail: result.error.detail,
msg: this.msg || this.arrayMsg?.element,
}),
);
}
}
return ok(input as InferSchemaType<S>[]);
}
}
class OptionalSchema<S extends Schema<any>>
extends BaseSchema<InferSchemaType<S> | undefined> {
constructor(
public readonly schema: S,
msg?: string,
) {
super(msg);
}
protected override validateInput(
input: unknown,
): Result<InferSchemaType<S> | undefined, SchemaValidationError> {
if (input === undefined) {
return ok(input);
}
return this.schema.parse(input);
}
}
class NullableSchema<S extends Schema<any>>
extends BaseSchema<InferSchemaType<S> | null> {
constructor(
public readonly schema: S,
msg?: string,
) {
super(msg);
}
protected override validateInput(
input: unknown,
): Result<InferSchemaType<S> | null, SchemaValidationError> {
if (input === null) {
return ok(input);
}
return this.schema.parse(input);
}
}
class NullishSchema<S extends Schema<any>>
extends BaseSchema<InferSchemaType<S> | undefined | null> {
constructor(
public readonly schema: S,
msg?: string,
) {
super(msg);
}
protected override validateInput(
input: unknown,
): Result<InferSchemaType<S> | undefined | null, SchemaValidationError> {
if (input === undefined || input === null) {
return ok(input);
}
return this.schema.parse(input);
}
}
class ResultSchema<T extends Schema<any>, E extends Schema<any>>
extends BaseSchema<Result<InferSchemaType<T>, InferSchemaType<E>>> {
constructor(
private readonly okSchema: T,
private readonly errSchema: E,
) {
super();
}
protected override validateInput(
input: unknown,
): Result<
Result<InferSchemaType<T>, InferSchemaType<E>>,
SchemaValidationError
> {
return BaseSchema.validatePrimitive<object>(input, "object").andThen(
(
obj,
): Result<
Result<InferSchemaType<T>, InferSchemaType<E>>,
SchemaValidationError
> => {
if ("tag" in obj) {
switch (obj.tag) {
case "ok": {
if ("value" in obj) {
return this.okSchema.parse(
obj.value,
).match(
(v) => ok(ok(v as InferSchemaType<T>)),
(e) =>
err(createValidationError(input, {
kind: "propertyValidation",
property: "value",
detail: e.detail,
})),
);
} else if (
BaseSchema.isNullishSchema(this.okSchema)
) {
return ok(
ok() as Result<
InferSchemaType<T>,
InferSchemaType<E>
>,
);
}
return err(createValidationError(input, {
kind: "missingProperties",
keys: ["value"],
msg: "If tag is set to 'ok', than result must contain a 'value' property",
}));
}
case "err": {
if (
"error" in obj
) {
return this.errSchema.parse(
obj.error,
).match(
(e) => ok(err(e as InferSchemaType<E>)),
(e) =>
err(createValidationError(input, {
kind: "propertyValidation",
property: "error",
detail: e.detail,
})),
);
} else if (
BaseSchema.isNullishSchema(this.errSchema)
) {
return ok(
err() as Result<
InferSchemaType<T>,
InferSchemaType<E>
>,
);
}
return err(createValidationError(input, {
kind: "missingProperties",
keys: ["error"],
msg: "If tag is set to 'err', than result must contain a 'error' property",
}));
}
default:
return err(createValidationError(input, {
kind: "propertyValidation",
property: "tag",
detail: {
kind: "typeMismatch",
expected: "'ok' or 'err'",
received: `'${obj.tag}'`,
},
}));
}
} else {
return err(createValidationError(input, {
kind: "missingProperties",
keys: ["tag"],
msg: "Result must contain a tag property",
}));
}
},
);
}
}
class OptionSchema<T extends Schema<any>>
extends BaseSchema<Option<InferSchemaType<T>>> {
constructor(
private readonly schema: T,
) {
super();
}
protected override validateInput(
input: unknown,
): Result<Option<InferSchemaType<T>>, SchemaValidationError> {
return BaseSchema.validatePrimitive<object>(input, "object").andThen(
(
obj,
): Result<Option<InferSchemaType<T>>, SchemaValidationError> => {
if ("tag" in obj) {
switch (obj.tag) {
case "some": {
if ("value" in obj) {
return this.schema.parse(
obj.value,
).match(
(v) => ok(some(v as InferSchemaType<T>)),
(e) =>
err(createValidationError(input, {
kind: "propertyValidation",
property: "value",
detail: e.detail,
})),
);
} else if (
BaseSchema.isNullishSchema(this.schema)
) {
return ok(some() as Option<InferSchemaType<T>>);
}
return err(createValidationError(input, {
kind: "missingProperties",
keys: ["value"],
msg: "If tag is set to 'some', than option must contain a 'value' property",
}));
}
case "none": {
return ok(none);
}
default:
return err(createValidationError(input, {
kind: "propertyValidation",
property: "tag",
detail: {
kind: "typeMismatch",
expected: "'some' or 'none'",
received: `'${obj.tag}'`,
},
}));
}
} else {
return err(createValidationError(input, {
kind: "missingProperties",
keys: ["tag"],
msg: "Option must contain a tag property",
}));
}
},
);
}
}
/* ── Helper Object for Schema Creation (z) ───────────────────────────────────── */
export const z = {
string: (msg?: string) => new StringSchema(msg),
literal: <L extends string>(lit: L, msg?: string) =>
new LiteralSchema<L>(lit, msg),
number: (msg?: string) => new NumberSchema(msg),
bigint: (msg?: string) => new BigintSchema(msg),
boolean: (msg?: string) => new BooleanSchema(msg),
date: (msg?: string) => new DateSchema(msg),
symbol: (msg?: string) => new SymbolSchema(msg),
undefined: (msg?: string) => new UndefinedSchema(msg),
null: (msg?: string) => new NullSchema(msg),
void: (msg?: string) => new VoidSchema(msg),
any: (msg?: string) => new AnySchema(msg),
unknown: (msg?: string) => new UnknownSchema(msg),
never: (msg?: string) => new NeverSchema(msg),
obj: <S extends Record<string, Schema<any>>>(
schema: S,
msg?: string | {
mismatch?: string;
nullObject?: string;
unexpectedProperty?: string;
propertyValidation?: string;
missingProperty?: string;
},
) => new ObjectSchema<S>(schema, msg),
union: <U extends Schema<any>[]>(
schemas: U,
msg?: string | {
mismatch?: string;
unionValidation?: string;
},
) => new UnionSchema<U>(schemas, msg),
array: <S extends Schema<any>>(
schema: S,
msg?: string | { mismatch?: string; element?: string },
) => new ArraySchema<S>(schema, msg),
optional: <S extends Schema<any>>(
schema: S,
msg?: string,
) => new OptionalSchema<S>(schema, msg),
nullable: <S extends Schema<any>>(
schema: S,
msg?: string,
) => new NullableSchema<S>(schema, msg),
nullish: <S extends Schema<any>>(
schema: S,
msg?: string,
) => new NullishSchema<S>(schema, msg),
result: <T extends Schema<any>, E extends Schema<any>>(
okSchema: T,
errSchema: E,
) => new ResultSchema<T, E>(okSchema, errSchema),
option: <T extends Schema<any>>(
schema: T,
) => new OptionSchema<T>(schema),
};
export type InferSchemaType<S> = S extends Schema<infer T> ? T : never;