diff --git a/eslint.config.mjs b/eslint.config.mjs index 5710d98..3fdc9a6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -32,7 +32,9 @@ export default [ "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-confusing-void-expression": "off", "@typescript-eslint/consistent-type-definitions": "off", + "@typescript-eslint/consistent-indexed-object-style": "off", "@typescript-eslint/prefer-return-this-type": "off", + "@typescript-eslint/unbound-method": "off", "@typescript-eslint/restrict-template-expressions": [ "error", { diff --git a/src/index.ts b/src/index.ts index ae40cf6..46721b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -54,6 +54,28 @@ type IssueLeaf = Readonly< } >; +function expectedType(expected: InputType[]): IssueLeaf { + return { + ok: false, + code: "invalid_type", + expected, + }; +} + +const ISSUE_EXPECTED_NOTHING = expectedType([]); +const ISSUE_EXPECTED_STRING = expectedType(["string"]); +const ISSUE_EXPECTED_NUMBER = expectedType(["number"]); +const ISSUE_EXPECTED_BIGINT = expectedType(["bigint"]); +const ISSUE_EXPECTED_BOOLEAN = expectedType(["boolean"]); +const ISSUE_EXPECTED_UNDEFINED = expectedType(["undefined"]); +const ISSUE_EXPECTED_NULL = expectedType(["null"]); +const ISSUE_EXPECTED_OBJECT = expectedType(["object"]); +const ISSUE_EXPECTED_ARRAY = expectedType(["array"]); +const ISSUE_MISSING_VALUE: IssueLeaf = { + ok: false, + code: "missing_value", +}; + type IssueTree = | Readonly<{ ok: false; code: "prepend"; key: Key; tree: IssueTree }> | Readonly<{ ok: false; code: "join"; left: IssueTree; right: IssueTree }> @@ -412,7 +434,6 @@ function isObject(v: unknown): v is Record { const FLAG_FORBID_EXTRA_KEYS = 0x1; const FLAG_STRIP_EXTRA_KEYS = 0x2; const FLAG_MISSING_VALUE = 0x4; -type Func = (v: unknown, flags: number) => RawResult; /** * Return the inferred output type of a validator. @@ -432,10 +453,82 @@ type ParseOptions = { mode?: "passthrough" | "strict" | "strip"; }; +const TAG_UNKNOWN = 0; +const TAG_NEVER = 1; +const TAG_STRING = 2; +const TAG_NUMBER = 3; +const TAG_BIGINT = 4; +const TAG_BOOLEAN = 5; +const TAG_NULL = 6; +const TAG_UNDEFINED = 7; +const TAG_LITERAL = 8; +const TAG_OPTIONAL = 9; +const TAG_OBJECT = 10; +const TAG_ARRAY = 11; +const TAG_UNION = 12; +const TAG_TRANSFORM = 13; + +type TaggedMatcher = { + tag: number; + match(value: unknown, flags: number): RawResult; +}; + +const taggedMatcher = ( + tag: number, + match: (value: unknown, flags: number) => RawResult, +): TaggedMatcher => { + return { tag, match }; +}; + +function callMatcher( + matcher: TaggedMatcher, + value: unknown, + flags: number, +): RawResult { + switch (matcher.tag) { + case TAG_UNKNOWN: + return undefined; + case TAG_NEVER: + return ISSUE_EXPECTED_NOTHING; + case TAG_STRING: + return typeof value === "string" ? undefined : ISSUE_EXPECTED_STRING; + case TAG_NUMBER: + return typeof value === "number" ? undefined : ISSUE_EXPECTED_NUMBER; + case TAG_BIGINT: + return typeof value === "bigint" ? undefined : ISSUE_EXPECTED_BIGINT; + case TAG_BOOLEAN: + return typeof value === "boolean" ? undefined : ISSUE_EXPECTED_BOOLEAN; + case TAG_NULL: + return value === null ? undefined : ISSUE_EXPECTED_NULL; + case TAG_UNDEFINED: + return value === undefined ? undefined : ISSUE_EXPECTED_UNDEFINED; + case TAG_LITERAL: + return matcher.match(value, flags); + case TAG_OPTIONAL: + return matcher.match(value, flags); + case TAG_OBJECT: + return matcher.match(value, flags); + case TAG_ARRAY: + return matcher.match(value, flags); + case TAG_UNION: + return matcher.match(value, flags); + case TAG_TRANSFORM: + return matcher.match(value, flags); + default: + return matcher.match(value, flags); + } +} + abstract class AbstractType { abstract readonly name: string; abstract toTerminals(func: (t: TerminalType) => void): void; - abstract func(v: unknown, flags: number): RawResult; + abstract createMatcher(): TaggedMatcher; + + get matcher(): TaggedMatcher { + const value = this.createMatcher(); + Object.defineProperty(this, "matcher", { value }); + return value; + } /** * Return new optional type that can not be used as a standalone @@ -469,7 +562,13 @@ abstract class AbstractType { optional( defaultFn?: () => T, ): Type | T> | Optional { - const optional = new Optional(this); + // If this type is already Optional there's no need to wrap it inside + // a new Optional instance. + const optional = + this.name === "optional" + ? (this as unknown as Optional) + : new Optional(this); + if (!defaultFn) { return optional; } @@ -550,14 +649,16 @@ abstract class Type extends AbstractType { * Parse a value without throwing. */ try(v: unknown, options?: ParseOptions): ValitaResult> { - let flags = FLAG_FORBID_EXTRA_KEYS; - if (options?.mode === "passthrough") { - flags = 0; - } else if (options?.mode === "strip") { - flags = FLAG_STRIP_EXTRA_KEYS; - } - - const r = this.func(v, flags); + const r = this.matcher.match( + v, + options === undefined + ? FLAG_FORBID_EXTRA_KEYS + : options.mode === "strip" + ? FLAG_STRIP_EXTRA_KEYS + : options.mode === "passthrough" + ? 0 + : FLAG_FORBID_EXTRA_KEYS, + ); if (r === undefined) { return { ok: true, value: v as Infer }; } else if (r.ok) { @@ -571,14 +672,16 @@ abstract class Type extends AbstractType { * Parse a value. Throw a ValitaError on failure. */ parse(v: unknown, options?: ParseOptions): Infer { - let flags = FLAG_FORBID_EXTRA_KEYS; - if (options?.mode === "passthrough") { - flags = 0; - } else if (options?.mode === "strip") { - flags = FLAG_STRIP_EXTRA_KEYS; - } - - const r = this.func(v, flags); + const r = this.matcher.match( + v, + options === undefined + ? FLAG_FORBID_EXTRA_KEYS + : options.mode === "strip" + ? FLAG_STRIP_EXTRA_KEYS + : options.mode === "passthrough" + ? 0 + : FLAG_FORBID_EXTRA_KEYS, + ); if (r === undefined) { return v as Infer; } else if (r.ok) { @@ -590,18 +693,22 @@ abstract class Type extends AbstractType { } class Nullable extends Type { - readonly name = "nullable"; + readonly name = "union"; constructor(private readonly type: Type) { super(); } - func(v: unknown, flags: number): RawResult { - return v === null ? undefined : this.type.func(v, flags); + createMatcher(): TaggedMatcher { + const matcher = this.type.matcher; + + return taggedMatcher(TAG_UNION, (v, flags) => + v === null ? undefined : callMatcher(matcher, v, flags), + ); } toTerminals(func: (t: TerminalType) => void): void { - func(nullSingleton); + func(null_() as TerminalType); this.type.toTerminals(func); } @@ -624,37 +731,21 @@ class Optional extends AbstractType { super(); } - func(v: unknown, flags: number): RawResult { - return v === undefined || flags & FLAG_MISSING_VALUE - ? undefined - : this.type.func(v, flags); + createMatcher(): TaggedMatcher { + const matcher = this.type.matcher; + + return taggedMatcher(TAG_OPTIONAL, (v, flags) => + v === undefined || flags & FLAG_MISSING_VALUE + ? undefined + : callMatcher(matcher, v, flags), + ); } toTerminals(func: (t: TerminalType) => void): void { func(this); - func(undefinedSingleton); + func(undefined_() as TerminalType); this.type.toTerminals(func); } - - optional( - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters - defaultFn: () => X, - ): Type | T>; - optional( - defaultFn: () => Exclude, - ): Type>; - optional(defaultFn: () => T): Type | T>; - optional(): Optional; - optional( - defaultFn?: () => T, - ): Type | T> | Optional { - if (!defaultFn) { - return this; - } - return new TransformType(this, (v) => { - return v === undefined ? { ok: true, value: defaultFn() } : undefined; - }); - } } type ObjectShape = Record; @@ -719,17 +810,6 @@ class ObjectType< > extends Type> { readonly name = "object"; - private _func?: ( - obj: Record, - flags: number, - ) => RawResult; - - private _invalidType: IssueLeaf = { - ok: false, - code: "invalid_type", - expected: ["object"], - }; - constructor( readonly shape: Shape, private readonly restType: Rest, @@ -741,6 +821,14 @@ class ObjectType< super(); } + createMatcher(): TaggedMatcher { + const func = createObjectMatcher(this.shape, this.restType, this.checks); + + return taggedMatcher(TAG_OBJECT, (v, flags) => + isObject(v) ? func(v, flags) : ISSUE_EXPECTED_OBJECT, + ); + } + check( func: (v: ObjectOutput) => boolean, error?: CustomError, @@ -755,19 +843,6 @@ class ObjectType< ]); } - func(v: unknown, flags: number): RawResult> { - if (!isObject(v)) { - return this._invalidType; - } - - let func = this._func; - if (func === undefined) { - func = createObjectMatcher(this.shape, this.restType, this.checks); - this._func = func; - } - return func(v, flags) as RawResult>; - } - rest(restType: R): ObjectType { return new ObjectType(this.shape, restType); } @@ -838,12 +913,15 @@ function createObjectMatcher( issue: IssueLeaf; }[], ): (v: Record, flags: number) => RawResult { - const missingValue = { - ok: false, - code: "missing_value", - } as const; + type Entry = { + key: string; + index: number; + matcher: TaggedMatcher; + optional: boolean; + missing: IssueTree; + }; - const indexedEntries = Object.keys(shape).map((key) => { + const indexedEntries = Object.keys(shape).map((key, index) => { const type = shape[key]; let optional = false as boolean; @@ -853,15 +931,25 @@ function createObjectMatcher( return { key, - type, + index, + matcher: type.matcher, optional, - missing: prependPath(key, missingValue), - }; + missing: prependPath(key, ISSUE_MISSING_VALUE), + } satisfies Entry; }); - if (indexedEntries.length === 0 && rest?.name === "unknown") { - // A fast path for record(unknown()) - return function (obj, _) { + const keyedEntries = Object.create(null) as { [K in string]?: Entry }; + indexedEntries.forEach((entry) => { + keyedEntries[entry.key] = entry; + }); + + const restMatcher = rest?.matcher; + + // A fast path for record(unknown()) + const fastPath = indexedEntries.length === 0 && rest?.name === "unknown"; + + return (obj, flags) => { + if (fastPath) { if (checks !== undefined) { for (let i = 0; i < checks.length; i++) { if (!checks[i].func(obj)) { @@ -870,24 +958,8 @@ function createObjectMatcher( } } return undefined; - }; - } - - const keyedEntries = Object.create(null) as Record< - string, - { index: number; type: AbstractType } | undefined - >; - indexedEntries.forEach((entry, index) => { - keyedEntries[entry.key] = { - index, - type: entry.type, - }; - }); - - const fallbackEntry = - rest === undefined ? undefined : { index: -1, type: rest }; + } - return function (obj, flags) { let copied = false; let output = obj; let issues: IssueTree | undefined; @@ -896,13 +968,14 @@ function createObjectMatcher( let seenCount = 0; if ( - flags & FLAG_FORBID_EXTRA_KEYS || - flags & FLAG_STRIP_EXTRA_KEYS || - fallbackEntry !== undefined + flags & (FLAG_FORBID_EXTRA_KEYS | FLAG_STRIP_EXTRA_KEYS) || + restMatcher !== undefined ) { for (const key in obj) { - const entry = keyedEntries[key] ?? fallbackEntry; - if (entry === undefined) { + const value = obj[key]; + + const entry = keyedEntries[key]; + if (entry === undefined && restMatcher === undefined) { if (flags & FLAG_FORBID_EXTRA_KEYS) { if (unrecognized === undefined) { unrecognized = [key]; @@ -926,8 +999,10 @@ function createObjectMatcher( continue; } - const value = obj[key]; - const r = entry.type.func(value, flags); + const r = + entry === undefined + ? callMatcher(restMatcher!, value, flags) + : callMatcher(entry.matcher, value, flags); if (r === undefined) { if (copied && issues === undefined) { set(output, key, value); @@ -938,7 +1013,7 @@ function createObjectMatcher( if (!copied) { output = {}; copied = true; - if (fallbackEntry === undefined) { + if (restMatcher === undefined) { for (let m = 0; m < indexedEntries.length; m++) { if (getBit(seenBits, m)) { const k = indexedEntries[m].key; @@ -954,7 +1029,7 @@ function createObjectMatcher( set(output, key, r.value); } - if (entry.index >= 0) { + if (entry !== undefined) { seenCount++; seenBits = setBit(seenBits, entry.index); } @@ -969,22 +1044,18 @@ function createObjectMatcher( const entry = indexedEntries[i]; const value = obj[entry.key]; - let keyFlags = flags & ~FLAG_MISSING_VALUE; + let extraFlags = 0; if (value === undefined && !(entry.key in obj)) { if (!entry.optional) { issues = joinIssues(issues, entry.missing); continue; } - keyFlags |= FLAG_MISSING_VALUE; + extraFlags = FLAG_MISSING_VALUE; } - const r = entry.type.func(value, keyFlags); + const r = callMatcher(entry.matcher, value, flags | extraFlags); if (r === undefined) { - if ( - copied && - issues === undefined && - !(keyFlags & FLAG_MISSING_VALUE) - ) { + if (copied && issues === undefined && !extraFlags) { set(output, entry.key, value); } } else if (!r.ok) { @@ -993,7 +1064,7 @@ function createObjectMatcher( if (!copied) { output = {}; copied = true; - if (fallbackEntry === undefined) { + if (restMatcher === undefined) { for (let m = 0; m < indexedEntries.length; m++) { if (m < i || getBit(seenBits, m)) { const k = indexedEntries[m].key; @@ -1062,78 +1133,71 @@ class ArrayOrTupleType< > extends Type> { readonly name = "array"; - private readonly restType: Type; - private readonly invalidType: IssueLeaf; - private readonly invalidLength: IssueLeaf; - private readonly minLength: number; - private readonly maxLength: number | undefined; - constructor( readonly prefix: Head, readonly rest: Rest | undefined, readonly suffix: Tail, ) { super(); + } - this.restType = rest ?? never(); - this.minLength = this.prefix.length + this.suffix.length; - this.maxLength = rest ? undefined : this.minLength; - this.invalidType = { - ok: false, - code: "invalid_type", - expected: ["array"], - }; - this.invalidLength = { + createMatcher(): TaggedMatcher { + const prefix = this.prefix.map((t) => t.matcher); + const suffix = this.suffix.map((t) => t.matcher); + const rest = + this.rest?.matcher ?? taggedMatcher(1, () => ISSUE_MISSING_VALUE); + + const minLength = prefix.length + suffix.length; + const maxLength = this.rest ? Infinity : minLength; + const invalidLength: IssueLeaf = { ok: false, code: "invalid_length", - minLength: this.minLength, - maxLength: this.maxLength, + minLength, + maxLength: maxLength === Infinity ? undefined : maxLength, }; - } - func(arr: unknown, flags: number): RawResult> { - if (!Array.isArray(arr)) { - return this.invalidType; - } + return taggedMatcher(TAG_ARRAY, (arr, flags) => { + if (!Array.isArray(arr)) { + return ISSUE_EXPECTED_ARRAY; + } - const length = arr.length; - const minLength = this.minLength; - const maxLength = this.maxLength ?? Infinity; - if (length < minLength || length > maxLength) { - return this.invalidLength; - } + const length = arr.length; + if (length < minLength || length > maxLength) { + return invalidLength; + } - const headEnd = this.prefix.length; - const tailStart = arr.length - this.suffix.length; - - let issueTree: IssueTree | undefined = undefined; - let output: unknown[] = arr; - for (let i = 0; i < arr.length; i++) { - const type = - i < headEnd - ? this.prefix[i] - : i >= tailStart - ? this.suffix[i - tailStart] - : this.restType; - const r = type.func(arr[i], flags); - if (r !== undefined) { - if (r.ok) { - if (output === arr) { - output = arr.slice(); + const headEnd = prefix.length; + const tailStart = arr.length - suffix.length; + + let issueTree: IssueTree | undefined = undefined; + let output: unknown[] = arr; + for (let i = 0; i < arr.length; i++) { + const entry = + i < headEnd + ? prefix[i] + : i >= tailStart + ? suffix[i - tailStart] + : rest; + const r = callMatcher(entry, arr[i], flags); + if (r !== undefined) { + if (r.ok) { + if (output === arr) { + output = arr.slice(); + } + output[i] = r.value; + } else { + issueTree = joinIssues(issueTree, prependPath(i, r)); } - output[i] = r.value; - } else { - issueTree = joinIssues(issueTree, prependPath(i, r)); } } - } - if (issueTree) { - return issueTree; - } else if (arr === output) { - return undefined; - } else { - return { ok: true, value: output as ArrayOutput }; - } + if (issueTree) { + return issueTree; + } else if (arr === output) { + return undefined; + } else { + return { ok: true, value: output }; + } + }); } concat(type: ArrayType | TupleType | VariadicTupleType): ArrayOrTupleType { @@ -1236,23 +1300,7 @@ function toInputType(v: unknown): InputType { } function dedup(arr: T[]): T[] { - return Array.from(new Set(arr)); -} - -function findCommonKeys(rs: ObjectShape[]): string[] { - const map = new Map(); - rs.forEach((r) => { - for (const key in r) { - map.set(key, (map.get(key) ?? 0) + 1); - } - }); - const result = [] as string[]; - map.forEach((count, key) => { - if (count === rs.length) { - result.push(key); - } - }); - return result; + return [...new Set(arr)]; } function groupTerminals( @@ -1321,7 +1369,7 @@ function groupTerminals( function createObjectKeyMatcher( objects: { root: AbstractType; terminal: ObjectType }[], key: string, -): Func | undefined { +): ((v: Record, f: number) => RawResult) | undefined { const list: { root: AbstractType; terminal: TerminalType }[] = []; for (const { root, terminal } of objects) { terminal.shape[key].toTerminals((t) => list.push({ root, terminal: t })); @@ -1343,14 +1391,14 @@ function createObjectKeyMatcher( } } - const missingValue = prependPath(key, { ok: false, code: "missing_value" }); + const missingValue = prependPath(key, ISSUE_MISSING_VALUE); const issue = prependPath( key, types.size === 0 ? { ok: false, code: "invalid_literal", - expected: Array.from(literals.keys()) as Literal[], + expected: [...literals.keys()] as Literal[], } : { ok: false, @@ -1359,33 +1407,38 @@ function createObjectKeyMatcher( }, ); - const litMap = - literals.size > 0 ? new Map() : undefined; - for (const [literal, options] of literals) { - litMap!.set(literal, options[0]); + const byLiteral = + literals.size > 0 ? new Map() : undefined; + if (byLiteral) { + for (const [literal, options] of literals) { + byLiteral.set(literal, options[0].matcher); + } } + const byType = - types.size > 0 ? ({} as Record) : undefined; - for (const [type, options] of types) { - byType![type] = options[0]; + types.size > 0 ? ({} as Record) : undefined; + if (byType) { + for (const [type, options] of types) { + byType[type] = options[0].matcher; + } } - return function (_obj: unknown, flags: number) { - const obj = _obj as Record; + const optional = optionals[0]?.matcher as TaggedMatcher | undefined; + return (obj, flags) => { const value = obj[key]; if (value === undefined && !(key in obj)) { - return optionals.length > 0 - ? optionals[0].func(obj, flags) - : missingValue; + return optional === undefined + ? missingValue + : callMatcher(optional, obj, flags); } - const option = byType?.[toInputType(value)] ?? litMap?.get(value); - return option ? option.func(obj, flags) : issue; + const option = byType?.[toInputType(value)] ?? byLiteral?.get(value); + return option ? callMatcher(option, obj, flags) : issue; }; } function createUnionObjectMatcher( terminals: { root: AbstractType; terminal: TerminalType }[], -): Func | undefined { +): ((v: Record, f: number) => RawResult) | undefined { if (terminals.some(({ terminal: t }) => t.name === "unknown")) { return undefined; } @@ -1399,8 +1452,16 @@ function createUnionObjectMatcher( return undefined; } - const shapes = objects.map(({ terminal }) => terminal.shape); - for (const key of findCommonKeys(shapes)) { + const keyCounts = new Map(); + for (const { terminal } of objects) { + for (const key in terminal.shape) { + keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1); + } + } + for (const [key, count] of keyCounts) { + if (count !== objects.length) { + continue; + } const matcher = createObjectKeyMatcher(objects, key); if (matcher) { return matcher; @@ -1411,7 +1472,7 @@ function createUnionObjectMatcher( function createUnionBaseMatcher( terminals: { root: AbstractType; terminal: TerminalType }[], -): Func { +): (v: unknown, f: number) => RawResult { const { expectedTypes, literals, types, unknowns, optionals } = groupTerminals(terminals); @@ -1420,7 +1481,7 @@ function createUnionBaseMatcher( ? { ok: false, code: "invalid_literal", - expected: Array.from(literals.keys()) as Literal[], + expected: [...literals.keys()] as Literal[], } : { ok: false, @@ -1428,23 +1489,39 @@ function createUnionBaseMatcher( expected: expectedTypes, }; - const litMap = literals.size > 0 ? literals : undefined; + const byLiteral = + literals.size > 0 ? new Map() : undefined; + if (byLiteral) { + for (const [literal, options] of literals) { + byLiteral.set( + literal, + options.map((t) => t.matcher), + ); + } + } + const byType = - types.size > 0 ? ({} as Record) : undefined; - for (const [type, options] of types) { - byType![type] = options; + types.size > 0 ? ({} as Record) : undefined; + if (byType) { + for (const [type, options] of types) { + byType[type] = options.map((t) => t.matcher); + } } - return function (value: unknown, flags: number) { + const optionalMatchers = optionals.map((t) => t.matcher); + const unknownMatchers = unknowns.map((t) => t.matcher); + return (value: unknown, flags: number) => { const options = flags & FLAG_MISSING_VALUE - ? optionals - : (byType?.[toInputType(value)] ?? litMap?.get(value) ?? unknowns); + ? optionalMatchers + : (byType?.[toInputType(value)] ?? + byLiteral?.get(value) ?? + unknownMatchers); let count = 0; let issueTree: IssueTree = issue; for (let i = 0; i < options.length; i++) { - const r = options[i].func(value, flags); + const r = callMatcher(options[i], value, flags); if (r === undefined || r.ok) { return r; } @@ -1460,7 +1537,6 @@ function createUnionBaseMatcher( class UnionType extends Type> { readonly name = "union"; - private _func?: Func>; constructor(readonly options: T) { super(); @@ -1472,30 +1548,18 @@ class UnionType extends Type> { }); } - func(v: unknown, flags: number): RawResult> { - let func = this._func; - if (func === undefined) { - const flattened: { root: AbstractType; terminal: TerminalType }[] = []; - this.options.forEach((option) => { - option.toTerminals((terminal) => { - flattened.push({ root: option, terminal }); - }); + createMatcher(): TaggedMatcher { + const flattened: { root: AbstractType; terminal: TerminalType }[] = []; + this.options.forEach((option) => { + option.toTerminals((terminal) => { + flattened.push({ root: option, terminal }); }); - const base = createUnionBaseMatcher(flattened); - const object = createUnionObjectMatcher(flattened); - if (!object) { - func = base as Func>; - } else { - func = function (v, f) { - if (isObject(v)) { - return object(v, f) as RawResult>; - } - return base(v, f) as RawResult>; - }; - } - this._func = func; - } - return func(v, flags); + }); + const base = createUnionBaseMatcher(flattened); + const object = createUnionObjectMatcher(flattened); + return taggedMatcher(TAG_UNION, (v, f) => + object !== undefined && isObject(v) ? object(v, f) : base(v, f), + ); } } @@ -1511,88 +1575,103 @@ const PASSTHROUGH = Object.freeze({ mode: "passthrough" }) as ParseOptions; class TransformType extends Type { readonly name = "transform"; - private transformChain?: TransformFunc[]; - private transformRoot?: AbstractType; - private readonly undef = ok(undefined); - constructor( protected readonly transformed: AbstractType, protected readonly transform: TransformFunc, ) { super(); - this.transformChain = undefined; - this.transformRoot = undefined; } - func(v: unknown, flags: number): RawResult { - let chain = this.transformChain; - if (!chain) { - chain = []; + createMatcher(): TaggedMatcher { + const chain: TransformFunc[] = []; - // eslint-disable-next-line @typescript-eslint/no-this-alias - let next: AbstractType = this; - while (next instanceof TransformType) { - chain.push(next.transform); - next = next.transformed; - } - chain.reverse(); - this.transformChain = chain; - this.transformRoot = next; + // eslint-disable-next-line @typescript-eslint/no-this-alias + let next: AbstractType = this; + while (next instanceof TransformType) { + chain.push(next.transform); + next = next.transformed; } + chain.reverse(); - let result = this.transformRoot!.func(v, flags); - if (result !== undefined && !result.ok) { - return result; - } + const matcher = next.matcher; + const undef = ok(undefined); - let current: unknown; - if (result !== undefined) { - current = result.value; - } else if (flags & FLAG_MISSING_VALUE) { - current = undefined; - result = this.undef; - } else { - current = v; - } + return taggedMatcher(TAG_TRANSFORM, (v, flags) => { + let result = callMatcher(matcher, v, flags); + if (result !== undefined && !result.ok) { + return result; + } - const options = - flags & FLAG_FORBID_EXTRA_KEYS - ? STRICT - : flags & FLAG_STRIP_EXTRA_KEYS - ? STRIP - : PASSTHROUGH; - for (let i = 0; i < chain.length; i++) { - const r = chain[i](current, options); - if (r !== undefined) { - if (!r.ok) { - return r; + let current: unknown; + if (result !== undefined) { + current = result.value; + } else if (flags & FLAG_MISSING_VALUE) { + current = undefined; + result = undef; + } else { + current = v; + } + + const options = + flags & FLAG_FORBID_EXTRA_KEYS + ? STRICT + : flags & FLAG_STRIP_EXTRA_KEYS + ? STRIP + : PASSTHROUGH; + for (let i = 0; i < chain.length; i++) { + const r = chain[i](current, options); + if (r !== undefined) { + if (!r.ok) { + return r; + } + current = r.value; + result = r; } - current = r.value; - result = r; } - } - return result as RawResult; + return result; + }); } toTerminals(func: (t: TerminalType) => void): void { this.transformed.toTerminals(func); } } + class LazyType extends Type { readonly name = "lazy"; private recursing = false; - private type?: Type; + private type?: AbstractType; + private typeMatcher?: TaggedMatcher; constructor(private readonly definer: () => Type) { super(); + this.type = undefined; + this.typeMatcher = undefined; + } + + get matcher() { + if (this.typeMatcher !== undefined) { + return this.typeMatcher; + } + return this.createMatcher(); } - func(v: unknown, flags: number): RawResult { - if (!this.type) { - this.type = this.definer(); + createMatcher(): TaggedMatcher { + let matcher = this.typeMatcher; + if (matcher === undefined) { + matcher = taggedMatcher(TAG_UNKNOWN, () => undefined); + this.typeMatcher = matcher; + + if (!this.type) { + this.type = this.definer(); + } + + const { tag, match } = this.type.matcher; + matcher.tag = tag; + matcher.match = match; } - return this.type.func(v, flags); + return matcher; } toTerminals(func: (t: TerminalType) => void): void { @@ -1611,176 +1690,104 @@ class LazyType extends Type { } } -class NeverType extends Type { - readonly name = "never"; - private readonly issue: IssueLeaf = { - ok: false, - code: "invalid_type", - expected: [], - }; - func(_: unknown, __: number): RawResult { - return this.issue; - } -} -const neverSingleton = new NeverType(); +function singleton( + name: string, + tag: number, + match: (value: unknown, flags: number) => RawResult, +): () => Type { + const value = taggedMatcher(tag, match); -/** - * Create a validator that never matches any value, - * analogous to the TypeScript type `never`. - */ -function never(): Type { - return neverSingleton; -} + class SimpleType extends Type { + readonly name: string; -class UnknownType extends Type { - readonly name = "unknown"; - func(_: unknown, __: number): RawResult { - return undefined; + constructor() { + super(); + this.name = name; + } + + createMatcher(): TaggedMatcher { + return value; + } } + Object.defineProperty(SimpleType.prototype, "matcher", { value }); + + const instance = new SimpleType(); + return () => instance; } -const unknownSingleton = new UnknownType(); /** * Create a validator that matches any value, * analogous to the TypeScript type `unknown`. */ -function unknown(): Type { - return unknownSingleton; -} - -class UndefinedType extends Type { - readonly name = "undefined"; - private readonly issue: IssueLeaf = { - ok: false, - code: "invalid_type", - expected: ["undefined"], - }; - func(v: unknown, _: number): RawResult { - return v === undefined ? undefined : this.issue; - } -} -const undefinedSingleton = new UndefinedType(); +const unknown = singleton("unknown", TAG_UNKNOWN, () => undefined); /** - * Create a validator that matches `undefined`. + * Create a validator that never matches any value, + * analogous to the TypeScript type `never`. */ -function undefined_(): Type { - return undefinedSingleton; -} - -class NullType extends Type { - readonly name = "null"; - private readonly issue: IssueLeaf = { - ok: false, - code: "invalid_type", - expected: ["null"], - }; - func(v: unknown, _: number): RawResult { - return v === null ? undefined : this.issue; - } -} -const nullSingleton = new NullType(); +const never = singleton( + "never", + TAG_NEVER, + () => ISSUE_EXPECTED_NOTHING, +); /** - * Create a validator that matches `null`. + * Create a validator that matches any string value. */ -function null_(): Type { - return nullSingleton; -} - -class NumberType extends Type { - readonly name = "number"; - private readonly issue: IssueLeaf = { - ok: false, - code: "invalid_type", - expected: ["number"], - }; - func(v: unknown, _: number): RawResult { - return typeof v === "number" ? undefined : this.issue; - } -} -const numberSingleton = new NumberType(); +const string = singleton("string", TAG_STRING, (v) => + typeof v === "string" ? undefined : ISSUE_EXPECTED_STRING, +); /** * Create a validator that matches any number value. */ -function number(): Type { - return numberSingleton; -} - -class BigIntType extends Type { - readonly name = "bigint"; - private readonly issue: IssueLeaf = { - ok: false, - code: "invalid_type", - expected: ["bigint"], - }; - func(v: unknown, _: number): RawResult { - return typeof v === "bigint" ? undefined : this.issue; - } -} -const bigintSingleton = new BigIntType(); +const number = singleton("number", TAG_NUMBER, (v) => + typeof v === "number" ? undefined : ISSUE_EXPECTED_NUMBER, +); /** * Create a validator that matches any bigint value. */ -function bigint(): Type { - return bigintSingleton; -} - -class StringType extends Type { - readonly name = "string"; - private readonly issue: IssueLeaf = { - ok: false, - code: "invalid_type", - expected: ["string"], - }; - func(v: unknown, _: number): RawResult { - return typeof v === "string" ? undefined : this.issue; - } -} -const stringSingleton = new StringType(); +const bigint = singleton("bigint", TAG_BIGINT, (v) => + typeof v === "bigint" ? undefined : ISSUE_EXPECTED_BIGINT, +); /** - * Create a validator that matches any string value. + * Create a validator that matches any boolean value. */ -function string(): Type { - return stringSingleton; -} +const boolean = singleton("boolean", TAG_BOOLEAN, (v) => + typeof v === "boolean" ? undefined : ISSUE_EXPECTED_BOOLEAN, +); -class BooleanType extends Type { - readonly name = "boolean"; - private readonly issue: IssueLeaf = { - ok: false, - code: "invalid_type", - expected: ["boolean"], - }; - func(v: unknown, _: number): RawResult { - return typeof v === "boolean" ? undefined : this.issue; - } -} -const booleanSingleton = new BooleanType(); +/** + * Create a validator that matches `null`. + */ +const null_ = singleton("null", TAG_NULL, (v) => + v === null ? undefined : ISSUE_EXPECTED_NULL, +); /** - * Create a validator that matches any boolean value. + * Create a validator that matches `undefined`. */ -function boolean(): Type { - return booleanSingleton; -} +const undefined_ = singleton("undefined", TAG_UNDEFINED, (v) => + v === undefined ? undefined : ISSUE_EXPECTED_UNDEFINED, +); class LiteralType extends Type { readonly name = "literal"; - private readonly issue: IssueLeaf; + constructor(readonly value: Out) { super(); - this.issue = { + } + + createMatcher(): TaggedMatcher { + const value = this.value; + const issue: IssueLeaf = { ok: false, code: "invalid_literal", expected: [value], }; - } - func(v: unknown, _: number): RawResult { - return v === this.value ? undefined : this.issue; + return taggedMatcher(TAG_LITERAL, (v) => (v === value ? undefined : issue)); } } @@ -1859,17 +1866,20 @@ function lazy(definer: () => Type): Type { } type TerminalType = - | NeverType - | UnknownType - | StringType - | NumberType - | BigIntType - | BooleanType - | UndefinedType - | NullType + | (Type & { + name: + | "unknown" + | "never" + | "string" + | "number" + | "bigint" + | "boolean" + | "null" + | "undefined"; + }) + | LiteralType | ObjectType | ArrayOrTupleType - | LiteralType | Optional; export {