diff --git a/package-lock.json b/package-lock.json index e955693..690168a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "peerDependencies": { "@sinclair/typebox": "^0.34.30", "valibot": "^1.0.0", - "zod": "^3.24.1" + "zod": "^3.25.64" } }, "node_modules/@andrewbranch/untar.js": { @@ -297,9 +297,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "dependencies": { "balanced-match": "^1.0.0" @@ -2146,9 +2146,9 @@ } }, "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "version": "3.25.64", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz", + "integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==", "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -2355,9 +2355,9 @@ "dev": true }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -3577,9 +3577,9 @@ "dev": true }, "zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "version": "3.25.64", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz", + "integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==", "peer": true } } diff --git a/package.json b/package.json index e5ed4bb..9fc675c 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "peerDependencies": { "@sinclair/typebox": "^0.34.30", "valibot": "^1.0.0", - "zod": "^3.24.1" + "zod": "^3.25.0" }, "devDependencies": { "@arethetypeswrong/cli": "^0.17.2", diff --git a/readme.md b/readme.md index c387643..7ff0f98 100644 --- a/readme.md +++ b/readme.md @@ -48,32 +48,38 @@ const result = validator['~standard'].validate({ // const result: StandardS TypeMap is a syntax frontend and compiler backend for the [TypeBox](https://github.com/sinclairzx81/typebox), [Valibot](https://github.com/fabian-hiller/valibot) and [Zod](https://github.com/colinhacks/zod) runtime type libraries. It offers a common TypeScript syntax for type construction, a runtime compiler for high-performance validation and type translation from one library to another. +TypeMap supports both Zod v3 and Zod v4, with distinct APIs for each version, allowing projects to smoothly transition between versions as needed. + TypeMap is written as an advanced type mapping system for the TypeBox project. It is designed to accelerate remote type libraries on TypeBox infrastructure as well enabling TypeBox to integrate with remote type library infrastructure via reverse type remapping. This project also offers high-performance validation for frameworks that orientate around the [Standard Schema](https://github.com/standard-schema/standard-schema) TypeScript interface. License: MIT ## Contents -- [Install](#Install) -- [Usage](#Usage) -- [Overview](#Overview) -- [Example](#Example) -- [Mapping](#Section-Mapping) - - [Syntax](#Mapping-Syntax) - - [TypeBox](#Mapping-TypeBox) - - [Valibot](#Mapping-Valibot) - - [Zod](#Mapping-Zod) -- [Syntax](#Section-Syntax) - - [Types](#Syntax-Types) - - [Options](#Syntax-Options) - - [Parameters](#Syntax-Parameters) - - [Generics](#Syntax-Generics) -- [Static](#Section-Static) -- [Json Schema](#Json-Schema) -- [Tree Shake](#Tree-Shake) -- [Compile](#Compile) -- [Benchmark](#Benchmark) -- [Contribute](#Contribute) +- [Install](#install) +- [Usage](#usage) +- [Overview](#overview) +- [Contents](#contents) +- [Example](#example) +- [Mapping](#mapping) + - [Syntax](#syntax) + - [TypeBox](#typebox) + - [Valibot](#valibot) + - [Zod](#zod) + - [Zod4](#zod4) +- [Syntax](#syntax-1) + - [Types](#types) + - [Options](#options) + - [Parameters](#parameters) + - [Generics](#generics) +- [Static](#static) +- [Json Schema](#json-schema) +- [Tree Shake](#tree-shake) +- [Compile](#compile) +- [Benchmark](#benchmark) + - [Test](#test) + - [Results](#results) +- [Contribute](#contribute) ## Example @@ -211,6 +217,21 @@ const V = Zod(v.string()) // const V: z.ZodString const Z = Zod(z.boolean()) // const Z: z.ZodBoolean (Zod) ``` + + +### Zod4 + +Use the `Zod4` function to translate types and syntax into Zod v4 types: + +```typescript +import { Zod4 } from '@sinclair/typemap' + +const S = Zod4('string[]') // const S: z.ZodArray<...> (Syntax) +const T = Zod4(t.Number()) // const T: z.ZodNumber (TypeBox) +const V = Zod4(v.string()) // const V: z.ZodString (Valibot) +const Z = Zod4(z.boolean()) // const Z: z.ZodBoolean (Zod) +``` + ## Syntax @@ -369,6 +390,20 @@ const T = TypeBoxFromZod(z.object({ // const T: TObject<{ })) // }> ``` +For Zod v4, you can use the corresponding import: + +```typescript +import { TypeBoxFromZod4 } from '@sinclair/typemap' // Use TypeBox & Zod v4, Tree Shake Valibot + +import { z } from 'zod/v4' + +const T = TypeBoxFromZod4(z.object({ // const T: TObject<{ + x: z.number(), // x: TNumber; + y: z.number(), // y: TNumber; + z: z.number() // z: TNumber; +})) // }> +``` + ## Compile Use the `Compile` function to compile TypeBox, Valibot and Zod on TypeBox infrastructure ([Example](https://www.typescriptlang.org/play/?moduleResolution=99&module=199#code/JYWwDg9gTgLgBAbzgYQuYAbApgGjgLQgBM4BfOAMyjTgHIABAZ2ADsBjDAQ2CgHoYAnmCwhOYWgCgJvXijRhMWAsTgAVIVilsILRvABunDMCKcY0OAF456bAApCROwAMEEuB4AeALjgsAriAARlhQOO4eAr4BwaHhHnAAXtGBIVASpM4AlFlaOnpwAEoAjFZwhsam5lAAdMgAFlhsANZ2SD5wxXhRcABMeMlwAMxkWQlwMnAAgmxsWNhQZlhEUpMAyjCcLKZQJGtsjaLSvMdwNedwAO71wAdwjPUQ-hgk9Zz6SiFYLH6cIMt0DZbHYkABqRhMZmgkgk2l08EKvTKFUh1QA2rQAH56YGcXa0AC6NRRVSwbTgHS6cB6-SSvhGpCyQA)) @@ -393,6 +428,22 @@ const R1 = validator.Check({ x: 1, y: 2, z: 3 }) // Accelerated const R2 = validator['~standard'].validate({ x: 1, y: 2, z: 3 }) ``` +You can also compile Zod v4 types: + +```typescript +import { Compile, Zod4 } from '@sinclair/typemap' + +// Compile Zod v4 Type + +const validator = Compile(Zod4(`{ + x: number, + y: number, + z: number +}`)) + +const R1 = validator.Check({ x: 1, y: 2, z: 3 }) // Accelerated with Zod v4 +``` + ## Benchmark This project manages a small benchmark that compares validation performance using Zod, Valibot, and TypeBox validators. For more comprehensive community benchmarks, refer to the [runtime-type-benchmarks](https://github.com/moltar/typescript-runtime-type-benchmarks) project. @@ -426,7 +477,32 @@ Results show the approximate elapsed time to complete the given iterations └─────────┴────────────────┴────────────────────┴────────────┴────────────┘ ``` - +Using [bun](https://bun.sh) and the latest (2025-06-14) versions of each library, the results show there is still an order-of-magnitude advantage to compiling. + +```ts +┌───┬──────────────┬──────────────────┬────────────┬──────────┐ +│ │ library │ using │ iterations │ elapsed │ +├───┼──────────────┼──────────────────┼────────────┼──────────┤ +│ 0 │ valibot │ valibot │ 10000000 │ 792 ms │ +│ 1 │ valibot │ typebox:value │ 10000000 │ 1625 ms │ +│ 2 │ valibot │ typebox:compile │ 10000000 │ 177 ms │ +└───┴──────────────┴──────────────────┴────────────┴──────────┘ +┌───┬──────────────┬──────────────────┬────────────┬──────────┐ +│ │ library │ using │ iterations │ elapsed │ +├───┼──────────────┼──────────────────┼────────────┼──────────┤ +│ 0 │ zod │ zod │ 10000000 │ 4553 ms │ +│ 1 │ zod │ typebox:value │ 10000000 │ 1739 ms │ +│ 2 │ zod │ typebox:compile │ 10000000 │ 214 ms │ +└───┴──────────────┴──────────────────┴────────────┴──────────┘ +┌───┬──────────────┬──────────────────┬────────────┬──────────┐ +│ │ library │ using │ iterations │ elapsed │ +├───┼──────────────┼──────────────────┼────────────┼──────────┤ +│ 0 │ zod v4 │ zod v4 │ 10000000 │ 1404 ms │ +│ 1 │ zod v4 │ typemap:zod4 │ 10000000 │ 1214 ms │ +│ 2 │ zod v4 │ typebox:value │ 10000000 │ 2294 ms │ +│ 3 │ zod v4 │ typebox:compile │ 10000000 │ 310 ms │ +└───┴──────────────┴──────────────────┴────────────┴──────────┘ +``` ## Contribute diff --git a/src/guard.ts b/src/guard.ts index de72462..677fdbb 100644 --- a/src/guard.ts +++ b/src/guard.ts @@ -29,6 +29,7 @@ THE SOFTWARE. import * as t from '@sinclair/typebox' import * as v from 'valibot' import * as z from 'zod' +import { z as z4 } from 'zod/v4' /** Structural Type for Syntax */ export type SyntaxType = string @@ -38,6 +39,8 @@ export type TypeBoxType = t.TSchema export type ValibotType = v.BaseSchema> /** Structural Type for Zod */ export type ZodType = z.ZodTypeAny | z.ZodEffects +/** Structural Type for Zod4 */ +export type Zod4Type = z4.ZodTypeAny // ------------------------------------------------------------------ // Syntax @@ -67,19 +70,61 @@ export function IsValibot(type: unknown): type is v.AnySchema { type['~standard'].vendor === 'valibot' ) } +// ------------------------------------------------------------------ +// Common Zod Detection +// ------------------------------------------------------------------ +/** + * Returns true if the given value has the standard Zod vendor property + * This is common to both Zod v3 and Zod v4 types + */ +// prettier-ignore +function IsZodVendor(type: unknown): boolean { + if (!t.ValueGuard.IsObject(type)) return false; + if (!t.ValueGuard.HasPropertyKey(type, '~standard')) return false; + + const standardProp = (type as any)['~standard']; + if (!t.ValueGuard.IsObject(standardProp)) return false; + if (!t.ValueGuard.HasPropertyKey(standardProp, 'vendor')) return false; + + return standardProp.vendor === 'zod'; +} + // ------------------------------------------------------------------ // Zod // ------------------------------------------------------------------ -/** Returns true if the given value is a Zod type */ +/** Returns true if the given value is a Zod v3 type */ // prettier-ignore export function IsZod(type: unknown): type is z.ZodTypeAny { + if (!IsZodVendor(type)) return false; + + const obj = type as Record; + + // Check for Zod v3 specific properties: + // 1. Has '_def' property (not 'def') + // 2. Does not have Zod v4 specific methods like 'meta' return ( - t.ValueGuard.IsObject(type) && - t.ValueGuard.HasPropertyKey(type, '~standard') && - t.ValueGuard.IsObject(type['~standard']) && - t.ValueGuard.HasPropertyKey(type['~standard'], 'vendor') && - type['~standard'].vendor === 'zod' - ) + t.ValueGuard.HasPropertyKey(obj, '_def') && + !t.ValueGuard.HasPropertyKey(obj, 'meta') + ); +} + +// ------------------------------------------------------------------ +// Zod4 +// ------------------------------------------------------------------ +/** Returns true if the given value is a Zod4 type */ +// prettier-ignore +export function IsZod4(type: unknown): type is z4.ZodTypeAny { + if (!IsZodVendor(type)) return false; + + const obj = type as Record; + + // Check Zod v4 specific properties: + // 1. Has 'def' property + // 2. Has specific v4 methods like 'meta' + return ( + t.ValueGuard.HasPropertyKey(obj, 'def') && + t.ValueGuard.HasPropertyKey(obj, 'meta') + ); } // ------------------------------------------------------------------ // Signature diff --git a/src/index.ts b/src/index.ts index 333972c..23c3e6c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,6 +45,7 @@ export * from './syntax/syntax-from-syntax' export * from './syntax/syntax-from-typebox' export * from './syntax/syntax-from-valibot' export * from './syntax/syntax-from-zod' +export * from './syntax/syntax-from-zod4' export { type TSyntax, Syntax } from './syntax/syntax' // ------------------------------------------------------------------ @@ -54,6 +55,7 @@ export * from './typebox/typebox-from-syntax' export * from './typebox/typebox-from-typebox' export * from './typebox/typebox-from-valibot' export * from './typebox/typebox-from-zod' +export * from './typebox/typebox-from-zod4' export { type TTypeBox, TypeBox } from './typebox/typebox' // ------------------------------------------------------------------ @@ -63,6 +65,7 @@ export * from './valibot/valibot-from-syntax' export * from './valibot/valibot-from-typebox' export * from './valibot/valibot-from-valibot' export * from './valibot/valibot-from-zod' +export * from './valibot/valibot-from-zod4' export { type TValibot, Valibot } from './valibot/valibot' // ------------------------------------------------------------------ @@ -72,4 +75,15 @@ export * from './zod/zod-from-syntax' export * from './zod/zod-from-typebox' export * from './zod/zod-from-valibot' export * from './zod/zod-from-zod' +export * from './zod/zod-from-zod4' export { type TZod, Zod } from './zod/zod' + +// ------------------------------------------------------------------ +// Zod4 +// ------------------------------------------------------------------ +export * from './zod4/zod4-from-syntax' +export * from './zod4/zod4-from-typebox' +export * from './zod4/zod4-from-valibot' +export * from './zod4/zod4-from-zod4' +export * from './zod4/zod4-from-zod' +export { type TZod4, Zod4 } from './zod4/zod4' diff --git a/src/regexp.ts b/src/regexp.ts new file mode 100644 index 0000000..6319ef9 --- /dev/null +++ b/src/regexp.ts @@ -0,0 +1,73 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typemap + +The MIT License (MIT) + +Copyright (c) 2024-2025 Haydn Paterson (sinclair) + +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. + +---------------------------------------------------------------------------*/ +export function combineRegExpHack(...patterns: (string|RegExp|undefined)[]) : string | undefined { + const strings = patterns.filter(p => p !== undefined) + .map(p => (p instanceof RegExp ? p.source : p as string)); + // Filter out undefined patterns + const validPatterns = Array.from(new Set(strings.filter(p => !!p + && p !== '.*' && p !== '^.*' && p !== '.*$' && p !== '^' && p !== '$' + && p !== '' && p !== '(.*)'))); // Remove empty strings + if (validPatterns.length === 0) return undefined; + if (validPatterns.length === 1) return validPatterns[0]; + const result = combinePatternsHack(validPatterns) +// if(process.env.NODE_ENV !== 'production') + // console.warn('Combining multiple patterns. This may not behave as expected.', [...validPatterns, '-->', result]); + return result +} + +// Should probably remove this if TypeBox supports pattern arrays + +function combinePatternsHack(patterns: string[]): string { + // Use lookahead assertions to enforce that a string matches all patterns + // This is more robust than trying to combine regex patterns directly + + // Handle patterns with different anchors + const lookaheadPatterns = patterns.map(pattern => { + // If the pattern has anchors, we need to preserve them in the lookahead + // Otherwise, we need to make sure it can match anywhere in the string + if (pattern.startsWith('^') && pattern.endsWith('$')) { + // Full string match pattern, use as is in lookahead + return `(?=${pattern})`; + } else if (pattern.startsWith('^')) { + // Beginning of string pattern + return `(?=${pattern})`; + } else if (pattern.endsWith('$')) { + // End of string pattern + return `(?=${pattern})`; + } else if (pattern.includes('.*')) { + // Already has wildcards, use as is + return `(?=${pattern})`; + } else { + // No anchors, can match anywhere + return `(?=.*${pattern}.*)`; + } + }); + + // Combine all lookaheads and ensure the pattern matches the entire string + return `^${lookaheadPatterns.join('')}.*$`; +} diff --git a/src/syntax/syntax-from-zod4.ts b/src/syntax/syntax-from-zod4.ts new file mode 100644 index 0000000..bbf9001 --- /dev/null +++ b/src/syntax/syntax-from-zod4.ts @@ -0,0 +1,48 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typemap + +The MIT License (MIT) + +Copyright (c) 2024-2025 Haydn Paterson (sinclair) + +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. + +---------------------------------------------------------------------------*/ + +import { type TTypeBoxFromZod4, TypeBoxFromZod4 } from '../typebox/typebox-from-zod4' +import { type TSyntaxFromTypeBox, SyntaxFromTypeBox } from './syntax-from-typebox' + +import * as t from '@sinclair/typebox' +import { z } from 'zod/v4' + +/** Creates Syntax from Zod v4 */ +// prettier-ignore +export type TSyntaxFromZod4, + Result extends string = TSyntaxFromTypeBox +> = Result + +/** Creates Syntax from Zod v4 */ +// prettier-ignore +export function SyntaxFromZod4(type: Type): TSyntaxFromZod4 { + const typebox = TypeBoxFromZod4(type) + const result = SyntaxFromTypeBox(typebox) + return result as never +} diff --git a/src/typebox/typebox-from-zod4.ts b/src/typebox/typebox-from-zod4.ts new file mode 100644 index 0000000..8094aff --- /dev/null +++ b/src/typebox/typebox-from-zod4.ts @@ -0,0 +1,660 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typemap + +The MIT License (MIT) + +Copyright (c) 2024-2025 Haydn Paterson (sinclair) + +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. + +---------------------------------------------------------------------------*/ + +import * as t from '@sinclair/typebox' +import { z } from 'zod/v4' +import { combineRegExpHack } from '../regexp.js' + +// ------------------------------------------------------------------ +// Options +// ------------------------------------------------------------------ +function Options(type: z.ZodType): t.SchemaOptions { + const description = typeof type.description === 'undefined' ? {} : { description: type.description } + return { ...description } +} +// ------------------------------------------------------------------ +// Formats +// ------------------------------------------------------------------ +const check = (type: z.ZodType, value: unknown) => type.safeParse(value).success + +// Register formats for Zod4 validators +// Note: In Zod v4, string formats are top-level functions rather than methods +// Types defined in zod/v4: +// // "email" | "url" | "emoji" | "uuid" | "guid" | "nanoid" | "cuid" | "cuid2" | "ulid" | "xid" | "ksuid" | "datetime" | "date" | "time" | "duration" | "ipv4" | "ipv6" | "cidrv4" | "cidrv6" | "base64" | "base64url" | "json_string" | "e164" | "lowercase" | "uppercase" | "regex" | "jwt" | "starts_with" | "ends_with" | "includes"; +t.FormatRegistry.Set('base64', (value) => check(z.base64(), value)) +t.FormatRegistry.Set('base64url', (value) => check(z.base64url(), value)) +t.FormatRegistry.Set('cidrv4', (value) => check(z.cidrv4(), value)) +t.FormatRegistry.Set('cidrv6', (value) => check(z.cidrv6(), value)) +t.FormatRegistry.Set('cuid', (value) => check(z.cuid(), value)) +t.FormatRegistry.Set('cuid2', (value) => check(z.cuid2(), value)) +t.FormatRegistry.Set('date', (value) => check(z.iso.date(), value)) +t.FormatRegistry.Set('datetime', (value) => check(z.iso.datetime(), value)) +t.FormatRegistry.Set('duration', (value) => check(z.iso.duration(), value)) +t.FormatRegistry.Set('e164', (value) => check(z.e164(), value)); +t.FormatRegistry.Set('email', (value) => check(z.email(), value)) +t.FormatRegistry.Set('emoji', (value) => check(z.emoji(), value)) +t.FormatRegistry.Set('guid', (value) => check(z.guid(), value)) +t.FormatRegistry.Set('ipv4', (value) => check(z.ipv4(), value)) +t.FormatRegistry.Set('ipv6', (value) => check(z.ipv6(), value)) +t.FormatRegistry.Set('json_string', (value) => check(z.json(), value)) +t.FormatRegistry.Set('json', (value) => check(z.json(), value)) +t.FormatRegistry.Set('jwt', (value) => check(z.jwt(), value)) +t.FormatRegistry.Set('ksuid', (value) => check(z.ksuid(), value)) +t.FormatRegistry.Set('lowercase', (value) => check(z.string().lowercase(), value)) +t.FormatRegistry.Set('nanoid', (value) => check(z.nanoid(), value)) +t.FormatRegistry.Set('time', (value) => check(z.iso.time(), value)) +t.FormatRegistry.Set('ulid', (value) => check(z.ulid(), value)) +t.FormatRegistry.Set('uppercase', (value) => check(z.string().uppercase(), value)) +t.FormatRegistry.Set('url', (value) => check(z.url(), value)) +t.FormatRegistry.Set('uuid', (value) => check(z.uuid(), value)) +t.FormatRegistry.Set('xid', (value) => check(z.xid(), value)) +// BTW, are these .Set() ops this going to fight the one in typebox-from-zod.ts? + +// ------------------------------------------------------------------ +// Any +// ------------------------------------------------------------------ +type TFromAny = Result +function FromAny(type: z.ZodAny): t.TSchema { + return t.Any(Options(type)) +} +// ------------------------------------------------------------------ +// Array +// ------------------------------------------------------------------ +type TFromArray>> = Result +function FromArray(type: z.ZodArray): t.TSchema { + const items = FromType(type.element) + const constraints = {} + return t.Array(items, { ...Options(type), ...constraints }) +} +// ------------------------------------------------------------------ +// BigInt +// ------------------------------------------------------------------ +type TFromBigInt = Result +function FromBigInt(type: z.ZodBigInt): t.TSchema { + return t.BigInt(Options(type)) +} +// ------------------------------------------------------------------ +// Boolean +// ------------------------------------------------------------------ +type TFromBoolean = Result +function FromBoolean(type: z.ZodBoolean): t.TSchema { + return t.Boolean(Options(type)) +} + +// ------------------------------------------------------------------ +// Catch (error capture with default) +// ------------------------------------------------------------------ +type TFromCatch> +> = Result; + +function FromCatch(type: z.ZodCatch): t.TSchema { + // Similar to default: treat as optional with default value + const inner = type.def.innerType as z.ZodType; + return t.Optional(FromType(inner)); +} + +// // ------------------------------------------------------------------ +// // Custom (including refinements) +// // ------------------------------------------------------------------ +// type TFromCustom +// > = Result; + +// function FromCustom(type: z.ZodCustom): t.TSchema { +// // Custom types in Zod4 are just regular types with a refinement +// // We can treat them as the inner type with no special handling +// const inner = type.def.fn; +// return t.Function(inner, Options(type)); +// } + +// ------------------------------------------------------------------ +// Date +// ------------------------------------------------------------------ +type TFromDate = Result +function FromDate(type: z.ZodDate): t.TSchema { + return t.Date(Options(type)) +} + +// ------------------------------------------------------------------ +// Default +// ------------------------------------------------------------------ +type TFromDefault> = Result +function FromDefault(type: z.ZodDefault): t.TSchema { + const inner = FromType(type.def.innerType as z.ZodType); + return t.Optional(inner, type.def.defaultValue) +} + +// ------------------------------------------------------------------ +// Discriminated Union - Deprecated in Zod4 +// ------------------------------------------------------------------ +// type TFromDiscriminatedUnion }> +// > = Result; + +// function FromDiscriminatedUnion( +// type: z.ZodDiscriminatedUnion +// ): t.TSchema { +// const options = type.options.map(opt => FromType(opt)); +// // z4 dropped discriminates, so just use a normal union +// return t.Union(options,Options(type)); +// } + +// ------------------------------------------------------------------ +// Enum +// ------------------------------------------------------------------ +/** prettier-ignore */ +type TFromEnum + = t.TUnion<{ [K in keyof Variants]: t.TLiteral }[keyof Variants][]> + +function FromEnum(type: z.ZodEnum): t.TSchema { + return t.Enum(type.enum, Options(type)) +} + +// ------------------------------------------------------------------ +// Intersect +// ------------------------------------------------------------------ +type TFromIntersect, TFromType]>> = Result +function FromIntersect(type: z.ZodIntersection): t.TSchema { + const left = FromType(type.def.left as z.ZodType) + const right = FromType(type.def.right as z.ZodType) + return t.Intersect([left, right], Options(type)) +} + +// ------------------------------------------------------------------ +// Lazy (Recursive Types) +// ------------------------------------------------------------------ +type TFromLazy>> = Result + +function FromLazy(type: z.ZodLazy): t.TSchema { + return t.Recursive((() => FromType(type.def.getter() as z.ZodType)), Options(type)); +} + +// ------------------------------------------------------------------ +// Literal +// ------------------------------------------------------------------ +type TFromLiteral> = Result +function FromLiteral(type: z.ZodLiteral): t.TSchema { + return t.Literal(type.value, Options(type)) +} + +// ------------------------------------------------------------------ +// Map - a TypeBox Map would be nice, but it's not JsonSchema +// ------------------------------------------------------------------ +// type TFromMap, TFromType]>> +// > = Result; + +// function FromMap(type: z.ZodMap): t.TSchema { +// const keySchema = FromType(type.def.keyType as z.ZodType); +// const valSchema = FromType(type.def.valueType as z.ZodType); +// return t.Array(t.Tuple([keySchema, valSchema]), Options(type)); +// } + +// ------------------------------------------------------------------ +// Never +// ------------------------------------------------------------------ +type TFromNever = t.TNever +function FromNever(_def: Def): t.TSchema { + return t.Never() +} + +// ------------------------------------------------------------------ +// Nullable +// ------------------------------------------------------------------ +type TFromNullable, t.TNull]> +> = Result; + +function FromNullable(type: z.ZodNullable): t.TSchema { + const inner = FromType(type.def.innerType as z.ZodType); + return t.Union([inner, t.Null()], Options(type)); +} + + +// ------------------------------------------------------------------ +// Null +// ------------------------------------------------------------------ +type TFromNull = Result +function FromNull(type: z.ZodNull): t.TSchema { + return t.Null(Options(type)) +} + +// ------------------------------------------------------------------ +// Number +// ------------------------------------------------------------------ +type TFromNumber = Result +function FromNumber(type: z.ZodNumber): t.TSchema { + const constraints = {} + // In Zod4 check minValue and maxValue properties if available + if (type.minValue !== null) { + Object.assign(constraints, { minimum: type.minValue }) + } + if (type.maxValue !== null) { + Object.assign(constraints, { maximum: type.maxValue }) + } + if (type.format === 'int' || type.format === 'integer') { + Object.assign(constraints, { type: 'integer' }) + } + return t.Number({ ...Options(type), ...constraints }) +} + +// ------------------------------------------------------------------ +// Object +// ------------------------------------------------------------------ +type TFromObject = t.Ensure< + t.TObject<{ + [Key in keyof Properties]: TFromType + }> +> +function FromObject(type: z.ZodObject): t.TSchema { + // Extract properties from Zod object type + const properties: Record = {} + const required: string[] = [] + + for (const [key, schema] of Object.entries(type.def.shape)) { + const zschema = schema as z.ZodType + properties[key] = FromType(zschema) + // Check if property is optional + if (!zschema.isOptional()) { + required.push(key) + } + } + + let objectOptions: t.SchemaOptions = { ...Options(type) } + + // Handle strict vs loose objects (additionalProperties) + if (type.constructor.name === 'ZodStrictObject') { + objectOptions = { ...objectOptions, additionalProperties: false } + } + + return t.Object(properties, { ...objectOptions, required }) +} +// ------------------------------------------------------------------ +// Optional +// ------------------------------------------------------------------ +type TFromOptional>> = Result +function FromOptional(type: z.ZodOptional): t.TSchema { + // Get the inner type and convert it first + const innerType = type.def.innerType as z.ZodType; + + // Default handling for other types + return t.Optional(FromType(innerType)); +} + +// // ------------------------------------------------------------------ +// // Pipe -- Too complicate, never seen it used +// // ------------------------------------------------------------------ +// type TFromPipe<_I extends z.ZodType, O extends z.ZodType, Result = TFromType> = Result; +// function FromPipe(type: z.ZodPipe): t.TSchema { +// // Get the input type and pass it through the FromType function +// const input = type.def.in as z.ZodType; +// return FromType(input); +// } + + +// ------------------------------------------------------------------ +// Promise +// ------------------------------------------------------------------ +type TFromPromise> +> = Result; + +function FromPromise(type: z.ZodPromise): t.TSchema { + return t.Promise(FromType(type.def.innerType as z.ZodType), Options(type)); +} + + +// ------------------------------------------------------------------ +// Readonly +// ------------------------------------------------------------------ +type TFromReadonly>> = Result +function FromReadonly(type: Type): t.TSchema { + return t.Readonly(FromType(type.def.innerType as z.ZodType)); +} + +// ------------------------------------------------------------------ +// Record +// ------------------------------------------------------------------ +type TFromRecord = t.Ensure, TFromType>> + +function FromRecord(type: z.ZodRecord): t.TSchema { + // Handle key and value types + const keyType = t.String() + const valueType = FromType(type.def.valueType as z.ZodType) + + return t.Record(keyType, valueType, Options(type)) +} + +// // ------------------------------------------------------------------ +// // Set - Like Map, I don't think TypeBox has a Set type +// // ------------------------------------------------------------------ +// type TFromSet> +// > = Result; + +// function FromSet(type: z.ZodSet): t.TSchema { +// const itemSchema = FromType(type._def.valueType as z.ZodType); +// return t.Array(itemSchema, { ...Options(type), uniqueItems: true }); +// } + +// ------------------------------------------------------------------ +// String +// ------------------------------------------------------------------ +type TFromString = Result + +function IsStringDef(def: z.core.$ZodTypeDef): def is z.core.$ZodStringDef { + return 'type' in def && def.type === 'string'; +} +function IsStringInternals(type: z.ZodType): type is z.ZodType & {_zod: z.core.$ZodStringInternals} { + return IsStringDef(type.def) && IsStringDef(type._zod.def); +} + +function IsStringFormatDef(def: z.core.$ZodStringDef): def is z.core.$ZodStringFormatDef { + return 'format' in def && typeof def.format === 'string' + && ('pattern' in def ? def.pattern instanceof RegExp : true); +} + +function FromStringLike( + type: z.ZodType & { _zod: z.core.$ZodStringInternals } +): t.TSchema { + const def = type._zod.def; + const _zod = type._zod as z.core.$ZodStringInternals; + const coerce = def.coerce; + const checkPatterns = + def.checks && def.checks.length > 0 ? getExtraPatterns(def.checks) : []; + // bag parsing + const bag = _zod.bag; + const bagPatterns = bag?.patterns ? Array.from(bag.patterns) : []; + const minLength = bag?.minimum; + const maxLength = bag?.maximum; + // string format + const format = IsStringFormatDef(def) + ? (def.format ?? bag?.format) + : bag?.format; + const fmtPattern = IsStringFormatDef(def) ? def.pattern : undefined; + const contentEncoding = bag?.contentEncoding; + /* Perhaps TypeBox has a better way to handle this, but I can only find one 'pattern' singular string */ + const pattern = combineRegExpHack( + fmtPattern, + ...checkPatterns, + ...bagPatterns + ); + const options: t.StringOptions = { + ...Options(type), + pattern, + minLength, + maxLength, + format, + contentEncoding, + coerce, + }; + return t.String(options); +} + + +// ------------------------------------------------------------------ +// String Formats +// ------------------------------------------------------------------ +// +// | Format | Special Properties in Internals (if any) | +// |----------------|--------------------------------------------------| +// | email | None | +// | url | `hostname?: RegExp`, `protocol?: RegExp` | +// | emoji | None | +// | uuid | `version?: "v1" \| "v2" \| "v3" \| "v4" \| "v5" \| "v6" \| "v7" \| "v8"` | +// | guid | None | +// | nanoid | None | +// | cuid | None | +// | cuid2 | None | +// | ulid | None | +// | xid | None | +// | ksuid | None | +// | datetime | `precision: number \| null`, `offset: boolean`, `local: boolean` | +// | date | None | +// | time | `precision?: number \| null` | +// | duration | None | +// | ipv4 | `version?: "v4"` | +// | ipv6 | `version?: "v6"` | +// | cidrv4 | `version?: "v4"` | +// | cidrv6 | `version?: "v6"` | +// | base64 | None | +// | base64url | None | +// | json_string | None (not defined as a schema in your list, only as a format) | +// | e164 | None | +// | lowercase | None (see $ZodCheckLowerCase) | +// | uppercase | None (see $ZodCheckUpperCase) | +// | regex | `pattern: RegExp` (see $ZodCheckRegex) | +// | jwt | `alg?: util.JWTAlgorithm` | +// | starts_with | `prefix: string` (see $ZodCheckStartsWith) | +// | ends_with | `suffix: string` (see $ZodCheckEndsWith) | +// | includes | `includes: string`, `position?: number` (see $ZodCheckIncludes) | + +// **Notes:** +// - For formats like `lowercase`, `uppercase`, `regex`, `starts_with`, `ends_with`, and `includes`, these are implemented as checks (see `$ZodCheckLowerCase`, `$ZodCheckUpperCase`, etc.), not as schemas in your main list. +// - All other formats are implemented as schemas with Internals extending `$ZodStringFormatInternals`, and any special properties are listed above. + +function getExtraPatterns(checks: z.core.$ZodCheck[]): (string|undefined)[]{ + return checks.map(item => { + const check = item._zod.def.check; + if(check === 'regex') return (item._zod.def as z.core.$ZodCheckRegexDef).pattern?.source; + // see packages/zod/src/v4/core/regexes.ts + if(check === 'lowercase') return '^[^A-Z]*$'; //regex for string with no uppercase letters + if(check === 'uppercase') return '^[^a-z]*$'; // regex for string with no lowercase letters + if(check === 'starts_with') return `^${(item._zod.def as z.core.$ZodCheckStartsWithDef).prefix}`; + if(check === 'ends_with') return `${(item._zod.def as z.core.$ZodCheckEndsWithDef).suffix}$`; + if(check === 'includes') return `.*${(item._zod.def as z.core.$ZodCheckIncludesDef).includes}.*`; + // TODO - warn or something if we hit an unknown check + return undefined; + }) +} + +// ------------------------------------------------------------------ +// Symbol +// ------------------------------------------------------------------ +type TFromSymbol = Result; + +function FromSymbol(type: z.ZodSymbol): t.TSchema { + return t.Symbol(Options(type)); +} + + +// ------------------------------------------------------------------ +// Template Literal +// ------------------------------------------------------------------ +// Helper to map Zod template literal part to TypeBox template literal kind +// (You may need to expand this as you support more Zod part types) + +type TFromTemplateLiteralPart = t.Ensure< + Part extends z.ZodTemplateLiteral ? t.TTemplateLiteralSyntax : + Part extends z.ZodString ? t.TString : + Part extends z.ZodNumber ? t.TNumber : + Part extends z.ZodBoolean ? t.TBoolean : + Part extends z.ZodLiteral ? t.TLiteral : + // Part extends z.ZodTemplateLiteral ? TFromTemplateLiteral : + t.TTemplateLiteralKind>; +type TFromTemplateLiteralParts = + { [K in keyof Parts]: TFromTemplateLiteralPart }[number][]; + +type TFromTemplateLiteral = + t.TTemplateLiteral>; +// z.templateLiteral +function FromTemplateLiteral(type: z.ZodTemplateLiteral): t.TSchema { + const parts = type.def.parts.map(part => FromType(part as z.ZodType)) as t.TTemplateLiteralKind[]; + return t.TemplateLiteral(parts, Options(type)); +} + +// ------------------------------------------------------------------ +// Transform +// ------------------------------------------------------------------ + +type TFromTransform = TFromType +function FromTransform(type: z.ZodTransform): t.TSchema { + // Get the input type and pass it through the FromType function + const output = type._zod.output as z.ZodType; + return FromType(output); +} +// ------------------------------------------------------------------ +// Tuple +// ------------------------------------------------------------------ +type TFromTuple = ( + Types extends [infer Left extends z.ZodType, ...infer Right extends z.ZodType[]] + ? TFromTuple]> + : t.TTuple +) + +function FromTuple(type: z.ZodTuple): t.TSchema { + const items = type.def.items.map(item => FromType(item as z.ZodType)) + return t.Tuple(items, Options(type)) +} +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +type TFromUnion }>> = Result +function FromUnion(type: z.ZodUnion): t.TSchema { + const options = type.options.map(option => FromType(option as z.ZodType)) + return t.Union(options, Options(type)) +} +// ------------------------------------------------------------------ +// Unknown +// ------------------------------------------------------------------ +type TFromUnknown = Result +function FromUnknown(type: z.ZodUnknown): t.TSchema { + return t.Unknown(Options(type)) +} +// ------------------------------------------------------------------ +// Undefined +// ------------------------------------------------------------------ +type TFromUndefined = Result +function FromUndefined(type: z.ZodUndefined): t.TSchema { + return t.Undefined(Options(type)) +} + +// ------------------------------------------------------------------ +// Void +// ------------------------------------------------------------------ +type TFromVoid = t.TVoid +function FromVoid(_def: Type): t.TSchema { + return t.Void() +} +// ------------------------------------------------------------------ +// Type +// ------------------------------------------------------------------ +// prettier-ignore +type TFromType = ( + Type extends z.ZodAny ? TFromAny : + Type extends z.ZodArray ? TFromArray : + Type extends z.ZodBigInt ? TFromBigInt : + Type extends z.ZodBoolean ? TFromBoolean : + Type extends z.ZodCatch ? TFromCatch : + // Type extends z.ZodCustom ? TFromCustom : // No idea how to handle in TypeBox + Type extends z.ZodDate ? TFromDate : + Type extends z.ZodDefault ? TFromDefault : + Type extends z.ZodDiscriminatedUnion ? TFromUnion : + Type extends z.ZodEnum ? TFromEnum : + Type extends z.ZodLazy ? TFromLazy : + Type extends z.ZodLiteral ? TFromLiteral : + // Type extends z.ZodMap ? TFromMap : // no such jsonschema + Type extends z.ZodNever ? TFromNever : + Type extends z.ZodNull ? TFromNull : + Type extends z.ZodNullable ? TFromNullable : + Type extends z.ZodNumber ? TFromNumber : + Type extends z.ZodObject ? TFromObject : + Type extends z.ZodOptional ? TFromOptional : + // Type extends z.ZodPipe ? TFromPipe : + Type extends z.ZodPromise ? TFromPromise

: + Type extends z.ZodRecord ? TFromRecord : + // Type extends z.ZodSet ? TFromSet : + Type extends z.ZodString ? TFromString : + Type extends z.ZodSymbol ? TFromSymbol : + Type extends z.ZodTemplateLiteral ? TFromTemplateLiteral : + Type extends z.ZodTransform ? TFromTransform : // suspicious + Type extends z.ZodTuple ? TFromTuple> : + Type extends z.ZodUndefined ? TFromUndefined : + Type extends z.ZodUnion ? TFromUnion : + Type extends z.ZodUnknown ? TFromUnknown : + Type extends z.ZodVoid ? TFromVoid : + // Intersection (Ensure Last Due to Zod Differentiation Issue) + Type extends z.ZodIntersection ? TFromIntersect : + + t.TNever +) + +// prettier-ignore +function FromType(type: z.ZodType): t.TSchema { + if (type instanceof z.ZodString + || type instanceof z.ZodStringFormat + || IsStringInternals(type)) { + return FromStringLike(type); + } + if (type instanceof z.ZodAny) return FromAny(type); + if (type instanceof z.ZodArray) return FromArray(type); + if (type instanceof z.ZodBigInt) return FromBigInt(type); + if (type instanceof z.ZodBoolean) return FromBoolean(type); + if (type instanceof z.ZodCatch) return FromCatch(type); + // if (type instanceof z.ZodCustom) return FromCustom(type); + if (type instanceof z.ZodDate) return FromDate(type); + if (type instanceof z.ZodDefault) return FromDefault(type); + if (type instanceof z.ZodDiscriminatedUnion) return FromUnion(type); + if (type instanceof z.ZodEnum) return FromEnum(type); + if (type instanceof z.ZodIntersection) return FromIntersect(type); + if (type instanceof z.ZodLazy) return FromLazy(type); + if (type instanceof z.ZodLiteral) return FromLiteral(type); + // if (type instanceof z.ZodMap) return FromMap(type); + if (type instanceof z.ZodNullable) return FromNullable(type); + if (type instanceof z.ZodNumber) return FromNumber(type); + if (type instanceof z.ZodObject) return FromObject(type); + if (type instanceof z.ZodOptional) return FromOptional(type); + // if (type instanceof z.ZodPipe) return FromPipe(type); + if (type instanceof z.ZodPromise) return FromPromise(type); + if (type instanceof z.ZodReadonly) return FromReadonly(type); + if (type instanceof z.ZodRecord) return FromRecord(type); + // if (type instanceof z.ZodSet) return FromSet(type); + if (type instanceof z.ZodSymbol) return FromSymbol(type); + if (type instanceof z.ZodTemplateLiteral) return FromTemplateLiteral(type); + if (type instanceof z.ZodTransform) return FromTransform(type); + if (type instanceof z.ZodTuple) return FromTuple(type); + if (type instanceof z.ZodUnion) return FromUnion(type); + if (type instanceof z.ZodUnknown) return FromUnknown(type); + if (type instanceof z.ZodUndefined) return FromUndefined(type); + if (type instanceof z.ZodVoid) return FromVoid(type); + return t.Never(); +} + +// ------------------------------------------------------------------ +// TypeBoxFromZod +// ------------------------------------------------------------------ +/** Creates a TypeBox type from Zod v4 */ +// prettier-ignore +export type TTypeBoxFromZod4 +> = Result +/** Creates a TypeBox type from Zod v4 */ +export function TypeBoxFromZod4(type: Type): TTypeBoxFromZod4 { + return FromType(type) as never +} diff --git a/src/typebox/typebox.ts b/src/typebox/typebox.ts index 9cbaaf4..acbbbf4 100644 --- a/src/typebox/typebox.ts +++ b/src/typebox/typebox.ts @@ -30,6 +30,7 @@ import { type TTypeBoxFromSyntax, TypeBoxFromSyntax } from './typebox-from-synta import { type TTypeBoxFromTypeBox, TypeBoxFromTypeBox } from './typebox-from-typebox' import { type TTypeBoxFromValibot, TypeBoxFromValibot } from './typebox-from-valibot' import { type TTypeBoxFromZod, TypeBoxFromZod } from './typebox-from-zod' +import { type TTypeBoxFromZod4, TypeBoxFromZod4 } from './typebox-from-zod4' import { type TSyntaxOptions } from '../options' import * as g from '../guard' @@ -71,6 +72,7 @@ export type TTypeBox, Type> : Type extends g.TypeBoxType ? TTypeBoxFromTypeBox : Type extends g.ValibotType ? TTypeBoxFromValibot : + Type extends g.Zod4Type ? TTypeBoxFromZod4 : Type extends g.ZodType ? TTypeBoxFromZod : t.TNever )> = Result @@ -86,6 +88,7 @@ export function TypeBox(...args: any[]): never { g.IsSyntax(type) ? TypeBoxFromSyntax(ContextFromParameter(parameter), type, options) : g.IsTypeBox(type) ? TypeBoxFromTypeBox(type) : g.IsValibot(type) ? TypeBoxFromValibot(type) : + g.IsZod4(type) ? TypeBoxFromZod4(type) : g.IsZod(type) ? TypeBoxFromZod(type) : t.Never() ) as never diff --git a/src/valibot/valibot-from-zod4.ts b/src/valibot/valibot-from-zod4.ts new file mode 100644 index 0000000..cb5be62 --- /dev/null +++ b/src/valibot/valibot-from-zod4.ts @@ -0,0 +1,53 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typemap + +The MIT License (MIT) + +Copyright (c) 2024-2025 Haydn Paterson (sinclair) + +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. + +---------------------------------------------------------------------------*/ + +import { type TTypeBoxFromZod4, TypeBoxFromZod4 } from '../typebox/typebox-from-zod4' +import { type TValibotFromTypeBox, ValibotFromTypeBox } from './valibot-from-typebox' + +import * as t from '@sinclair/typebox' +import * as v from 'valibot' +import { z } from 'zod/v4' + +// ------------------------------------------------------------------ +// ValibotFromZod4 +// ------------------------------------------------------------------ +/** Creates a Valibot type from Zod v4 */ +// prettier-ignore +export type TValibotFromZod4, + Result extends v.BaseSchema = TValibotFromTypeBox +> = Result +/** Creates a Valibot type from Zod v4 */ +// prettier-ignore +export function ValibotFromZod4 = TValibotFromZod4 +>(type: Type): Result { + const schema = TypeBoxFromZod4(type) + const result = ValibotFromTypeBox(schema) + return result as never +} diff --git a/src/zod/zod-from-zod4.ts b/src/zod/zod-from-zod4.ts new file mode 100644 index 0000000..23cbc1b --- /dev/null +++ b/src/zod/zod-from-zod4.ts @@ -0,0 +1,53 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typemap + +The MIT License (MIT) + +Copyright (c) 2024-2025 Haydn Paterson (sinclair) + +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. + +---------------------------------------------------------------------------*/ + +import { type TTypeBoxFromZod4, TypeBoxFromZod4 } from '../typebox/typebox-from-zod4' +import { type TZodFromTypeBox, ZodFromTypeBox } from './zod-from-typebox' + +import * as t from '@sinclair/typebox' +import * as z from 'zod' +import { z as z4 } from 'zod/v4' + +// ------------------------------------------------------------------ +// ZodFromZod4 +// ------------------------------------------------------------------ +/** Creates a Zod v3 type from Zod v4 */ +// prettier-ignore +export type TZodFromZod4, + Result extends z.ZodTypeAny | z.ZodEffects = TZodFromTypeBox +> = Result +/** Creates a Zod v3 type from Zod v4 */ +// prettier-ignore +export function ZodFromZod4 = TZodFromZod4 +>(type: Type): Result { + const schema = TypeBoxFromZod4(type) + const result = ZodFromTypeBox(schema) + return result as never +} diff --git a/src/zod/zod.ts b/src/zod/zod.ts index 7bb96b3..f647cda 100644 --- a/src/zod/zod.ts +++ b/src/zod/zod.ts @@ -30,6 +30,7 @@ import { type TZodFromSyntax, ZodFromSyntax } from './zod-from-syntax' import { type TZodFromTypeBox, ZodFromTypeBox } from './zod-from-typebox' import { type TZodFromValibot, ZodFromValibot } from './zod-from-valibot' import { type TZodFromZod, ZodFromZod } from './zod-from-zod' +import { type TZodFromZod4, ZodFromZod4 } from './zod-from-zod4' import { type TSyntaxOptions } from '../options' import { type TParameter, type TContextFromParameter, ContextFromParameter } from '../typebox/typebox' @@ -47,6 +48,7 @@ export type TZod : Type extends g.ValibotType ? TZodFromValibot : Type extends g.ZodType ? TZodFromZod : + Type extends g.Zod4Type ? TZodFromZod4 : z.ZodNever )> = Result @@ -65,6 +67,7 @@ export function Zod(...args: any[]): never { g.IsTypeBox(type) ? ZodFromTypeBox(type) : g.IsValibot(type) ? ZodFromValibot(type) : g.IsZod(type) ? ZodFromZod(type) : + g.IsZod4(type) ? ZodFromZod4(type) : z.never() ) as never } diff --git a/src/zod4/zod4-from-syntax.ts b/src/zod4/zod4-from-syntax.ts new file mode 100644 index 0000000..d36be1f --- /dev/null +++ b/src/zod4/zod4-from-syntax.ts @@ -0,0 +1,48 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typemap + +The MIT License (MIT) + +Copyright (c) 2024-2025 Haydn Paterson (sinclair) + +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. + +---------------------------------------------------------------------------*/ + +import { TTypeBoxFromSyntax, TypeBoxFromSyntax } from '../typebox/typebox-from-syntax' +import { Zod4FromTypeBox, TZod4FromTypeBox } from './zod4-from-typebox' +import * as t from '@sinclair/typebox' +import { z } from 'zod/v4' + +// ------------------------------------------------------------------ +// Zod4FromSyntax +// ------------------------------------------------------------------ +/** Creates a Zod v4 type from Syntax */ +// prettier-ignore +export type TZod4FromSyntax, + Result extends z.ZodTypeAny | z.ZodNever = TZod4FromTypeBox +> = Result +/** Creates a Zod v4 type from Syntax */ +export function Zod4FromSyntax(context: Context, type: Type, options?: t.SchemaOptions): TZod4FromSyntax { + const typebox = TypeBoxFromSyntax(context, type, options) + const result = Zod4FromTypeBox(typebox) + return result as never +} diff --git a/src/zod4/zod4-from-typebox.ts b/src/zod4/zod4-from-typebox.ts new file mode 100644 index 0000000..e42b0f6 --- /dev/null +++ b/src/zod4/zod4-from-typebox.ts @@ -0,0 +1,433 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typemap + +The MIT License (MIT) + +Copyright (c) 2024-2025 Haydn Paterson (sinclair) + +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. + +---------------------------------------------------------------------------*/ + +import * as t from '@sinclair/typebox' +import z4 from 'zod/dist/types/v4' +import { z } from 'zod/v4' + +// ------------------------------------------------------------------ +// Constraint +// ------------------------------------------------------------------ +type TConstraint = (input: Input) => Output + +// ------------------------------------------------------------------ +// Any +// ------------------------------------------------------------------ +type TFromAny = Result +function FromAny(_type: t.TAny): z.ZodType { + return z.any() +} +// ------------------------------------------------------------------ +// Array +// ------------------------------------------------------------------ +type TFromArray>> = Result +function FromArray(type: t.TArray): z.ZodType { + const constraints: TConstraint>[] = [] + const { minItems, maxItems /* minContains, maxContains, contains */ } = type + if (t.ValueGuard.IsNumber(minItems)) constraints.push((input) => input.min(minItems)) + if (t.ValueGuard.IsNumber(maxItems)) constraints.push((input) => input.max(maxItems)) + const mapped = z.array(FromType(type.items)) + return constraints.reduce((type, constraint) => constraint(type), mapped) +} +// ------------------------------------------------------------------ +// BigInt +// ------------------------------------------------------------------ +type TFromBigInt = Result +function FromBigInt(_type: t.TBigInt): z.ZodType { + return z.bigint() +} +// ------------------------------------------------------------------ +// Boolean +// ------------------------------------------------------------------ +type TFromBoolean = Result +function FromBoolean(_type: t.TBoolean): z.ZodType { + return z.boolean() +} +// ------------------------------------------------------------------ +// Date +// ------------------------------------------------------------------ +type TFromDate = Result +function FromDate(_type: t.TDate): z.ZodType { + return z.date() +} +// ------------------------------------------------------------------ +// Function +// ------------------------------------------------------------------ +// note: Zod v4 does not support function types, so we use a workaround +// z.custom[0]>((fn) => schema.implement(fn)) +// https://github.com/colinhacks/zod/issues/4143#issuecomment-2845134912 +// prettier-ignore +type TFromFunction +> = ( + MappedParameters extends [z.ZodType, ...z.ZodType[]] | [] + ? z.ZodType<(...args: z.output>) => z.output>> + : z.ZodNever +) +function FromFunction(type: t.TFunction): z.ZodType { + const mappedParameters = FromTypes(type.parameters) + const input = z.tuple(mappedParameters as [z.ZodType, ...z.ZodType[]]) + const output = FromType(type.returns) + + return z.custom().transform((arg, ctx) => { + if (typeof arg !== 'function') { + ctx.addIssue({ + code: "invalid_type", + expected: 'custom', + received: typeof arg, + input: arg + }) + return z.NEVER + } + + // Use function schema internally for validation + const functionSchema = z.function({ + input, + output + }) + + return functionSchema.implement(arg as (...args: any[]) => any) + }) +} +// ------------------------------------------------------------------ +// Integer +// ------------------------------------------------------------------ +type TFromInteger = Result +function FromInteger(type: t.TInteger): z.ZodType { + const { exclusiveMaximum, exclusiveMinimum, minimum, maximum, multipleOf } = type + const constraints: TConstraint[] = [(value) => value.int()] + if (t.ValueGuard.IsNumber(exclusiveMinimum)) constraints.push((input) => input.min(exclusiveMinimum + 1)) + if (t.ValueGuard.IsNumber(exclusiveMaximum)) constraints.push((input) => input.min(exclusiveMaximum - 1)) + if (t.ValueGuard.IsNumber(maximum)) constraints.push((input) => input.max(maximum)) + if (t.ValueGuard.IsNumber(minimum)) constraints.push((input) => input.min(minimum)) + if (t.ValueGuard.IsNumber(multipleOf)) constraints.push((input) => input.multipleOf(multipleOf)) + return constraints.reduce((input, constraint) => constraint(input), z.number()) +} +// ------------------------------------------------------------------ +// Intersect +// ------------------------------------------------------------------ +// prettier-ignore +type TFromIntersect = ( + Types extends [infer Left extends t.TSchema, ...infer Right extends t.TSchema[]] + ? TFromIntersect, Result>> + : Result +) +function FromIntersect(type: t.TIntersect): z.ZodType { + return type.allOf.reduce((result, left) => { + return z.intersection(FromType(left), result) as never + }, z.unknown()) as never +} +// ------------------------------------------------------------------ +// Literal +// ------------------------------------------------------------------ +type TFromLiteral> = Result +function FromLiteral(type: t.TLiteral): z.ZodType { + return z.literal(type.const) +} +// ------------------------------------------------------------------ +// Object +// ------------------------------------------------------------------ +// prettier-ignore +type TFromObject< Properties extends t.TProperties, + Result = z.ZodObject<{ + [Key in keyof Properties]: TFromType + }>, +> = Result +// prettier-ignore +function FromObject(type: t.TObject): z.ZodType { + const constraints: TConstraint>[] = [] + const { additionalProperties } = type + if (additionalProperties === false) constraints.push((input) => input.strict()) + if (t.KindGuard.IsSchema(additionalProperties)) constraints.push((input) => input.catchall(FromType(additionalProperties))) + const properties = globalThis.Object.getOwnPropertyNames(type.properties).reduce((result, key) => ({ ...result, [key]: FromType(type.properties[key]) }), {}) + return constraints.reduce((type, constraint) => constraint(type), z.object(properties)) +} +// ------------------------------------------------------------------ +// Promise +// ------------------------------------------------------------------ +type TFromPromise>> = Result +function FromPromise(type: t.TPromise): z.ZodType { + return z.promise(FromType(type.item)) +} +// ------------------------------------------------------------------ +// Record +// ------------------------------------------------------------------ +type TFromRegExp = Result +function FromRegExp(type: t.TRegExp): z.ZodType { + const constraints: TConstraint[] = [(input) => input.regex(new RegExp(type.source), type.flags)] + const { minLength, maxLength } = type + if (t.ValueGuard.IsNumber(maxLength)) constraints.push((input) => input.max(maxLength)) + if (t.ValueGuard.IsNumber(minLength)) constraints.push((input) => input.min(minLength)) + return constraints.reduce((type, constraint) => constraint(type), z.string()) +} +// ------------------------------------------------------------------ +// Record +// ------------------------------------------------------------------ +// prettier-ignore +type TFromRecord = ( + TFromType extends infer ZodKey extends z4.core.$ZodRecordKey + ? z.ZodRecord> + : z.ZodNever +) +// prettier-ignore +function FromRecord(type: t.TRecord): z.ZodType { + const pattern = globalThis.Object.getOwnPropertyNames(type.patternProperties)[0] + const value = FromType(type.patternProperties[pattern]) + return ( + pattern === t.PatternNumberExact ? z.record(z.number(), value) : + pattern === t.PatternStringExact ? z.record(z.string(), value) : + z.record(z.string().regex(new RegExp(pattern)), value) + ) +} +// ------------------------------------------------------------------ +// Never +// ------------------------------------------------------------------ +type TFromNever = Result +function FromNever(type: t.TNever): z.ZodType { + return z.never() +} +// ------------------------------------------------------------------ +// Never +// ------------------------------------------------------------------ +type TFromNull = Result +function FromNull(_type: t.TNull): z.ZodType { + return z.null() +} +// ------------------------------------------------------------------ +// Number +// ------------------------------------------------------------------ +type TFromNumber = Result +function FromNumber(type: t.TNumber): z.ZodType { + const { exclusiveMaximum, exclusiveMinimum, minimum, maximum, multipleOf } = type + const constraints: TConstraint[] = [] + if (t.ValueGuard.IsNumber(exclusiveMinimum)) constraints.push((input) => input.min(exclusiveMinimum + 1)) + if (t.ValueGuard.IsNumber(exclusiveMaximum)) constraints.push((input) => input.min(exclusiveMaximum - 1)) + if (t.ValueGuard.IsNumber(maximum)) constraints.push((input) => input.max(maximum)) + if (t.ValueGuard.IsNumber(minimum)) constraints.push((input) => input.min(minimum)) + if (t.ValueGuard.IsNumber(multipleOf)) constraints.push((input) => input.multipleOf(multipleOf)) + return constraints.reduce((input, constraint) => constraint(input), z.number()) +} +// ------------------------------------------------------------------ +// String +// ------------------------------------------------------------------ +type TFromString = Result +// prettier-ignore +function FromString(type: t.TString): z.ZodType { + const constraints: TConstraint[] = [] + const { minLength, maxLength, pattern, format } = type + const input = t.ValueGuard.IsString(format) ? + format === 'base64' ? z.base64() : + format === 'base64url' ? z.base64url() : + format === 'cidrv4' ? z.cidrv4() : + format === 'cidrv6' ? z.cidrv6() : + format === 'cidr' ? z.union([z.cidrv4(), z.cidrv6()]) : + format === 'cuid' ? z.cuid() : + format === 'cuid2' ? z.cuid2() : + format === 'date' ? z.date() : + format === 'datetime' ? z.iso.datetime() : + format === 'duration' ? z.iso.duration() : + format === 'email' ? z.email() : + format === 'emoji' ? z.emoji() : + format === 'ipv4' ? z.ipv4() : + format === 'ipv6' ? z.ipv6() : + format === 'ip' ? z.union([z.ipv4(), z.ipv6()]) : + format === 'jwt' ? z.jwt() : + format === 'nanoid' ? z.nanoid() : + format === 'time' ? z.iso.time() : + format === 'ulid' ? z.ulid() : + format === 'url' ? z.url() : + format === 'uuid' ? z.uuid() : + z.string() : z.string(); + + if (t.ValueGuard.IsNumber(maxLength)) constraints.push((input) => input.max(maxLength)) + if (t.ValueGuard.IsNumber(minLength)) constraints.push((input) => input.min(minLength)) + if (t.ValueGuard.IsString(pattern)) constraints.push((input) => input.regex(new RegExp(pattern))) + return input instanceof z.ZodString + ? constraints.reduce((type, constraint) => constraint(type), input) + : input; +} +// ------------------------------------------------------------------ +// Symbol +// ------------------------------------------------------------------ +type TFromSymbol = Result +function FromSymbol(_type: t.TSymbol): z.ZodType { + return z.symbol() +} +// ------------------------------------------------------------------ +// Tuple +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTuple> = ( + Mapped extends [z.ZodType, ...z.ZodType[]] | [] + ? z.ZodTuple + : z.ZodNever +) +function FromTuple(type: t.TTuple): z.ZodType { + const mapped = FromTypes(type.items || []); + return z.tuple(mapped as [z.ZodType, ...z.ZodType[]]) +} +// ------------------------------------------------------------------ +// Undefined +// ------------------------------------------------------------------ +type TFromUndefined = Result +function FromUndefined(_type: t.TUndefined): z.ZodType { + return z.undefined() +} +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +// prettier-ignore +type TFromUnion> = ( + Mapped extends z.ZodUnion ? z.ZodUnion : z.ZodNever +) +function FromUnion(_type: t.TUnion): z.ZodType { + const mapped = FromTypes(_type.anyOf) as [z.ZodType, z.ZodType, ...z.ZodType[]] + return mapped.length >= 1 ? z.union(mapped) : z.never() +} +// ------------------------------------------------------------------ +// TUnknown +// ------------------------------------------------------------------ +type TFromUnknown = Result +function FromUnknown(_type: t.TUnknown): z.ZodType { + return z.unknown() +} +// ------------------------------------------------------------------ +// Void +// ------------------------------------------------------------------ +type TFromVoid = Result +function FromVoid(_type: t.TVoid): z.ZodType { + return z.void() +} +// ------------------------------------------------------------------ +// Types +// ------------------------------------------------------------------ +// prettier-ignore +type TFromTypes = ( + Types extends [infer Left extends t.TSchema, ...infer Right extends t.TSchema[]] + ? TFromTypes]> + : Result +) +function FromTypes(types: t.TSchema[]): Readonly { + return types.map((type) => FromType(type)) as Readonly +} +// ------------------------------------------------------------------ +// Type +// ------------------------------------------------------------------ +// prettier-ignore +type TFromType ? TFromArray : + Type extends t.TBigInt ? TFromBigInt : + Type extends t.TBoolean ? TFromBoolean : + Type extends t.TDate ? TFromDate : + Type extends t.TFunction ? TFromFunction : + Type extends t.TInteger ? TFromInteger : + Type extends t.TIntersect ? TFromIntersect : + Type extends t.TLiteral ? TFromLiteral : + Type extends t.TNever ? TFromNever : + Type extends t.TNull ? TFromNull : + Type extends t.TNumber ? TFromNumber : + Type extends t.TObject ? TFromObject : + Type extends t.TPromise ? TFromPromise : + Type extends t.TRecord ? TFromRecord : + Type extends t.TRegExp ? TFromRegExp : + Type extends t.TString ? TFromString : + Type extends t.TSymbol ? TFromSymbol : + Type extends t.TTuple ? TFromTuple : + Type extends t.TUndefined ? TFromUndefined : + Type extends t.TUnion ? TFromUnion : + Type extends t.TUnknown ? TFromUnknown : + Type extends t.TVoid ? TFromVoid : + z.ZodNever + ), + // Modifier Mapping + IsReadonly extends boolean = Type extends t.TReadonly ? true : false, + IsOptional extends boolean = Type extends t.TOptional ? true : false, + Result extends z.ZodType | z.ZodNever = ( + [IsReadonly, IsOptional] extends [true, true] ? z.ZodReadonly> : + [IsReadonly, IsOptional] extends [false, true] ? z.ZodOptional : + [IsReadonly, IsOptional] extends [true, false] ? z.ZodReadonly : + Mapped + ) +> = Result +// prettier-ignore +function FromType(type: t.TSchema): z.ZodType | z.ZodNever { + const constraints: TConstraint[] = [] + if(!t.ValueGuard.IsUndefined(type.description)) constraints.push(input => input.describe(type.description!)) + if(!t.ValueGuard.IsUndefined(type.default)) constraints.push(input => input.default(type.default)) + const mapped = constraints.reduce((type, constraint) => constraint(type), ( + t.KindGuard.IsAny(type) ? FromAny(type) : + t.KindGuard.IsArray(type) ? FromArray(type) : + t.KindGuard.IsBigInt(type) ? FromBigInt(type) : + t.KindGuard.IsBoolean(type) ? FromBoolean(type) : + t.KindGuard.IsDate(type) ? FromDate(type) : + t.KindGuard.IsFunction(type) ? FromFunction(type) : + t.KindGuard.IsInteger(type) ? FromInteger(type) : + t.KindGuard.IsIntersect(type) ? FromIntersect(type) : + t.KindGuard.IsLiteral(type) ? FromLiteral(type) : + t.KindGuard.IsNever(type) ? FromNever(type) : + t.KindGuard.IsNull(type) ? FromNull(type) : + t.KindGuard.IsNumber(type) ? FromNumber(type) : + t.KindGuard.IsObject(type) ? FromObject(type) : + t.KindGuard.IsPromise(type) ? FromPromise(type) : + t.KindGuard.IsRegExp(type) ? FromRegExp(type) : + t.KindGuard.IsRecord(type) ? FromRecord(type) : + t.KindGuard.IsString(type) ? FromString(type) : + t.KindGuard.IsSymbol(type) ? FromSymbol(type) : + t.KindGuard.IsTuple(type) ? FromTuple(type) : + t.KindGuard.IsUndefined(type) ? FromUndefined(type) : + t.KindGuard.IsUnion(type) ? FromUnion(type) : + t.KindGuard.IsUnknown(type) ? FromUnknown(type) : + t.KindGuard.IsVoid(type) ? FromVoid(type) : + z.never() + )) + // Modifier Mapping + const isOptional = t.KindGuard.IsOptional(type) + const isReadonly = t.KindGuard.IsReadonly(type) + const result = ( + isOptional && isReadonly ? z.optional(mapped) : + isOptional && !isReadonly ? z.optional(mapped) : + !isOptional && isReadonly ? mapped : + mapped + ) + return result +} +// ------------------------------------------------------------------ +// ZodFromTypeBox +// ------------------------------------------------------------------ +/** Creates a Zod type from TypeBox */ +// prettier-ignore +export type TZod4FromTypeBox +> = Result +/** Creates a Zod v4 type from TypeBox */ +export function Zod4FromTypeBox(type: Type): TZod4FromTypeBox { + return FromType(type) as never +} diff --git a/src/zod4/zod4-from-valibot.ts b/src/zod4/zod4-from-valibot.ts new file mode 100644 index 0000000..ba324df --- /dev/null +++ b/src/zod4/zod4-from-valibot.ts @@ -0,0 +1,49 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typemap + +The MIT License (MIT) + +Copyright (c) 2024-2025 Haydn Paterson (sinclair) + +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. + +---------------------------------------------------------------------------*/ + +import { type TTypeBoxFromValibot, TypeBoxFromValibot } from '../typebox/typebox-from-valibot' +import { type TZod4FromTypeBox, Zod4FromTypeBox } from './zod4-from-typebox' + +import * as t from '@sinclair/typebox' +import * as v from 'valibot' +import { z } from 'zod/v4' + +/** Creates a Zod v4 type from Valibot */ +// prettier-ignore +export type TZod4FromValibot, + TypeBox extends t.TSchema = TTypeBoxFromValibot, + Result extends z.ZodTypeAny | z.ZodNever = TZod4FromTypeBox +> = Result + +/** Creates a Zod v4 type from Valibot */ +// prettier-ignore +export function Zod4FromValibot>(type: Type): TZod4FromValibot { + const typebox = TypeBoxFromValibot(type) + const result = Zod4FromTypeBox(typebox) + return result +} diff --git a/src/zod4/zod4-from-zod.ts b/src/zod4/zod4-from-zod.ts new file mode 100644 index 0000000..847ba2a --- /dev/null +++ b/src/zod4/zod4-from-zod.ts @@ -0,0 +1,53 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typemap + +The MIT License (MIT) + +Copyright (c) 2024-2025 Haydn Paterson (sinclair) + +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. + +---------------------------------------------------------------------------*/ + +import { type TTypeBoxFromZod, TypeBoxFromZod } from '../typebox/typebox-from-zod' +import { type TZod4FromTypeBox, Zod4FromTypeBox } from './zod4-from-typebox' + +import * as t from '@sinclair/typebox' +import * as z from 'zod' +import { z as z4 } from 'zod/v4' + +// ------------------------------------------------------------------ +// Zod4FromZod +// ------------------------------------------------------------------ +/** Creates a Zod v4 type from Zod v3 */ +// prettier-ignore +export type TZod4FromZod, + TypeBox extends t.TSchema = TTypeBoxFromZod, + Result extends z4.ZodType | z.ZodNever = TZod4FromTypeBox +> = Result +/** Creates a Zod v4 type from Zod v3 */ +// prettier-ignore +export function Zod4FromZod, + Result extends z4.ZodType | z.ZodNever = TZod4FromZod +>(type: Type): Result { + const schema = TypeBoxFromZod(type) + const result = Zod4FromTypeBox(schema) + return result as never +} diff --git a/src/zod4/zod4-from-zod4.ts b/src/zod4/zod4-from-zod4.ts new file mode 100644 index 0000000..b3bf6f6 --- /dev/null +++ b/src/zod4/zod4-from-zod4.ts @@ -0,0 +1,41 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typemap + +The MIT License (MIT) + +Copyright (c) 2024-2025 Haydn Paterson (sinclair) + +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. + +---------------------------------------------------------------------------*/ + +import { z } from 'zod/v4' + +/** Creates a Zod v4 type from Zod v4 */ +// prettier-ignore +export type TZod4FromZod4 = Result + +/** Creates a Zod v4 type from Zod v4 */ +// prettier-ignore +export function Zod4FromZod4(type: Type): TZod4FromZod4 { + return type as never +} diff --git a/src/zod4/zod4.ts b/src/zod4/zod4.ts new file mode 100644 index 0000000..ab9eef9 --- /dev/null +++ b/src/zod4/zod4.ts @@ -0,0 +1,73 @@ +/*-------------------------------------------------------------------------- + +@sinclair/typemap + +The MIT License (MIT) + +Copyright (c) 2024-2025 Haydn Paterson (sinclair) + +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. + +---------------------------------------------------------------------------*/ + +import { type TZod4FromSyntax, Zod4FromSyntax } from './zod4-from-syntax' +import { type TZod4FromTypeBox, Zod4FromTypeBox } from './zod4-from-typebox' +import { type TZod4FromValibot, Zod4FromValibot } from './zod4-from-valibot' +import { type TZod4FromZod4, Zod4FromZod4 } from './zod4-from-zod4' +import { type TZod4FromZod, Zod4FromZod } from './zod4-from-zod' +import { type TSyntaxOptions } from '../options' + +import { type TParameter, type TContextFromParameter, ContextFromParameter } from '../typebox/typebox' + +import * as g from '../guard' +import { z } from 'zod/v4' + +// ------------------------------------------------------------------ +// Zod4 +// ------------------------------------------------------------------ +/** Creates a Zod v4 type by mapping from a remote Type */ +// prettier-ignore +export type TZod4, Type> : + Type extends g.TypeBoxType ? TZod4FromTypeBox : + Type extends g.ValibotType ? TZod4FromValibot : + Type extends g.ZodType ? TZod4FromZod : + Type extends g.Zod4Type ? TZod4FromZod4 : + z.ZodNever +)> = Result + +/** Creates a Zod v4 type by mapping from a remote Type */ +export function Zod4(parameter: Parameter, type: Type, options?: TSyntaxOptions): TZod4 +/** Creates a Zod v4 type by mapping from a remote Type */ +export function Zod4(type: Type, options?: TSyntaxOptions): TZod4<{}, Type> +/** Creates a Zod v4 type by mapping from a remote Type */ +export function Zod4(type: Type, options?: TSyntaxOptions): TZod4<{}, Type> +/** Creates a Zod v4 type by mapping from a remote Type */ +// prettier-ignore +export function Zod4(...args: any[]): never { + const [parameter, type, options] = g.Signature(args) + return ( + g.IsSyntax(type) ? Zod4FromSyntax(ContextFromParameter(parameter), type, options) : + g.IsTypeBox(type) ? Zod4FromTypeBox(type) : + g.IsValibot(type) ? Zod4FromValibot(type) : + g.IsZod(type) ? Zod4FromZod(type) : + g.IsZod4(type) ? Zod4FromZod4(type) : + z.never() + ) as never +} diff --git a/task/benchmark/index.ts b/task/benchmark/index.ts index f071ea0..bffaaec 100644 --- a/task/benchmark/index.ts +++ b/task/benchmark/index.ts @@ -1,8 +1,9 @@ import { Value } from '@sinclair/typebox/value' -import { Compile, TypeBox } from '@sinclair/typemap' +import { Compile, TypeBox, TypeBoxFromZod4, Zod4 } from '@sinclair/typemap' import * as v from 'valibot' import * as z from 'zod' +import * as z4 from 'zod/v4' // ------------------------------------------------------------------ // Benchmark @@ -76,7 +77,50 @@ function valibot_using_compiler() { return benchmark('valibot', 'typebox:compile', () => T.Check({ x: 'hello', y: 42, z: true })) } +// ------------------------------------------------------------------ +// Zod v4 +// ------------------------------------------------------------------ +function zod4() { + const T = z4.object({ + x: z4.string(), + y: z4.number(), + z: z4.boolean(), + }) + return benchmark('zod v4', 'zod v4', () => T.safeParse({ x: 'hello', y: 42, z: true }).success) +} +function zod4_using_value() { + const T = TypeBox( + z4.object({ + x: z4.string(), + y: z4.number(), + z: z4.boolean(), + }), + ) + return benchmark('zod v4', 'typebox:value', () => Value.Check(T, { x: 'hello', y: 42, z: true })) +} +function zod4_using_compiler() { + const T = Compile( + z4.object({ + x: z4.string(), + y: z4.number(), + z: z4.boolean(), + }), + ) + return benchmark('zod v4', 'typebox:compile', () => T.Check({ x: 'hello', y: 42, z: true })) +} + +// Direct API comparison +function zod4_using_library_api() { + const T = Zod4(`{ + x: string, + y: number, + z: boolean + }`) + return benchmark('zod v4', 'typemap:zod4', () => T.safeParse({ x: 'hello', y: 42, z: true }).success) +} + console.log('running benchmark') console.table([valibot(), valibot_using_value(), valibot_using_compiler()]) console.table([zod(), zod_using_value(), zod_using_compiler()]) +console.table([zod4(), zod4_using_library_api(), zod4_using_value(), zod4_using_compiler(),]) diff --git a/test/index.ts b/test/index.ts index a6814d5..2e56b20 100644 --- a/test/index.ts +++ b/test/index.ts @@ -5,3 +5,7 @@ import './typebox-from-zod' import './typebox-from-valibot' import './valibot-from-typebox' import './zod-from-typebox' +import './typebox-from-zod4' +import './zod4-from-typebox' +import './zod-detection-test' +import './zod4-from-zod-and-zod-from-zod4' diff --git a/test/typebox-from-zod.ts b/test/typebox-from-zod.ts index ff76ec3..0e8dfa4 100644 --- a/test/typebox-from-zod.ts +++ b/test/typebox-from-zod.ts @@ -1,9 +1,9 @@ -import { TypeBox } from '@sinclair/typemap' -import { TypeGuard } from '@sinclair/typebox' +import { TypeBox, Compile } from '@sinclair/typemap' +import { TypeGuard , TSchema} from '@sinclair/typebox' import { Assert } from './assert' import * as t from '@sinclair/typebox' import * as z from 'zod' - +const ideally = process.env.EXTRA_TESTING === 'true' ? it : it.skip describe('TypeBox From Zod', () => { // ---------------------------------------------------------------- // Metadata @@ -467,4 +467,274 @@ describe('TypeBox From Zod', () => { const T = TypeBox(z.void()) Assert.IsTrue(TypeGuard.IsVoid(T)) }) + // ---------------------------------------------------------------- + // String Validation Behavior Tests + // ---------------------------------------------------------------- + + describe('String Validation Behavior', () => { + const Validator = (s: TSchema)=>{ + const S = Compile(s); + return { + AssertValidateIsTrue: (v: string) => { + const result = S.Check(v); + const err = result ? "is-valid" : `Validation was false (expected true) for value: '${v}' with schema:${JSON.stringify(s , null, 2)}`; + Assert.IsEqual(err, "is-valid"); + }, + AssertValidateIsFalse: (v: string) => { + const result = S.Check(v); + const err = !result ? "not-valid" : `Validation was true (expected false) for value: '${v}' with schema:${JSON.stringify(s , null, 2)}`; + Assert.IsEqual(err, "not-valid"); + } + } + } + + // Tests for single string checks + describe('Basic String Checks', () => { + it('Should validate startsWith correctly', () => { + const schema = TypeBox(z.string().startsWith('prefix')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that start with 'prefix' + AssertValidateIsTrue('prefix123'); + AssertValidateIsTrue('prefixABC'); + + // Should reject strings that don't start with 'prefix' + AssertValidateIsFalse('wrongprefix'); + AssertValidateIsFalse('wrong'); + }); + + it('Should validate endsWith correctly', () => { + const schema = TypeBox(z.string().endsWith('suffix')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that end with 'suffix' + AssertValidateIsTrue('123suffix'); + AssertValidateIsTrue('ABCsuffix'); + + // Should reject strings that don't end with 'suffix' + AssertValidateIsFalse('suffixwrong'); + AssertValidateIsFalse('wrong'); + }); + ideally('Should validate includes correctly', () => { + const schema = TypeBox(z.string().includes('middle')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that include 'middle' + AssertValidateIsTrue('start-middle-end'); + AssertValidateIsTrue('middleAtStart'); + AssertValidateIsTrue('atTheMiddle'); + + // Should reject strings that don't include 'middle' + AssertValidateIsFalse('nomid'); + AssertValidateIsFalse('midle'); // Misspelled + }); + ideally('Should validate regex patterns correctly', () => { + const schema = TypeBox(z.string().regex(/^A[BC]+D$/)); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that match the regex + AssertValidateIsTrue('ABCD'); + AssertValidateIsTrue('AABBBCD'); + AssertValidateIsTrue('AABCD'); + AssertValidateIsTrue('AABBBBCD'); + // Should reject strings that don't match the regex + AssertValidateIsFalse('AC'); + AssertValidateIsFalse('123'); + AssertValidateIsFalse('mixed123'); + }); + + // Tests for combined string checks + describe('Combined String Checks', () => { + it('Should validate startsWith + endsWith correctly', () => { + const schema = TypeBox(z.string().startsWith('start').endsWith('end')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that start with 'start' AND end with 'end' + AssertValidateIsTrue('startend'); + AssertValidateIsTrue('start_middle_end'); + + // Should reject strings that don't meet both conditions + AssertValidateIsFalse('startonly'); + AssertValidateIsFalse('endonly'); + AssertValidateIsFalse('neither'); + }); + ideally('Should validate startsWith + includes correctly', () => { + const schema = TypeBox(z.string().startsWith('start').includes('middle')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that start with 'start' AND include 'middle' + AssertValidateIsTrue('startmiddle'); + AssertValidateIsTrue('start_middle_end'); + + // Should reject strings that don't meet both conditions + AssertValidateIsFalse('startonly'); + AssertValidateIsFalse('middleonly'); + AssertValidateIsFalse('neither'); + }); + ideally('Should validate endsWith + includes correctly', () => { + const schema = TypeBox(z.string().endsWith('end').includes('middle')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that end with 'end' AND include 'middle' + AssertValidateIsTrue('middleend'); + AssertValidateIsTrue('start_middle_end'); + + // Should reject strings that don't meet both conditions + AssertValidateIsFalse('endonly'); + AssertValidateIsFalse('middleonly'); + AssertValidateIsFalse('neither'); + }); + ideally('Should validate startsWith + endsWith + includes correctly', () => { + const schema = TypeBox(z.string().startsWith('start').endsWith('end').includes('middle')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that meet all three conditions + AssertValidateIsTrue('startmiddleend'); + AssertValidateIsTrue('start_middle_end'); + + // Should reject strings that don't meet all conditions + AssertValidateIsFalse('startend'); // Missing 'middle' + AssertValidateIsFalse('startmiddle'); // Missing 'end' + AssertValidateIsFalse('middlewithend'); // Missing 'start' + AssertValidateIsFalse('neither'); + }); + }); + + // Tests for special cases mentioned in the comments + describe('Special Case Combinations', () => { + it('Should validate ipv4 with startsWith correctly', () => { + // This tests the commented case "z.ipv4().startsWith('192.168.')" + const schema = TypeBox(z.string().ip("v4").startsWith('192.168.')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept IPv4 addresses that start with '192.168.' + AssertValidateIsTrue('192.168.1.1'); + AssertValidateIsTrue('192.168.0.1'); + AssertValidateIsTrue('192.168.254.254'); + + // Should reject IPv4 addresses that don't start with '192.168.' + AssertValidateIsFalse('10.0.0.1'); + AssertValidateIsFalse('172.16.0.1'); + + // Should reject strings that start with '192.168.' but aren't valid IPv4s + AssertValidateIsFalse('192.168.'); + AssertValidateIsFalse('192.168.1'); + AssertValidateIsFalse('192.168.1.'); + AssertValidateIsFalse('192.168.256.1'); + }); + ideally('Should validate regex with startsWith correctly', () => { + const schema = TypeBox(z.string().regex(/^[a-z]+$/).startsWith('abc')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that are lowercase AND start with 'abc' + AssertValidateIsTrue('abc'); + AssertValidateIsTrue('abcdef'); + + // Should reject strings that don't meet both conditions + AssertValidateIsFalse('abcDEF'); // Not all lowercase + AssertValidateIsFalse('def'); // Doesn't start with 'abc' + }); + + it('Should validate email with endsWith correctly', () => { + const schema = TypeBox(z.string().email().endsWith('@example.com')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept emails that end with '@example.com' + AssertValidateIsTrue('user@example.com'); + AssertValidateIsTrue('another.user@example.com'); + + // Should reject emails that don't end with '@example.com' + AssertValidateIsFalse('user@gmail.com'); + AssertValidateIsFalse('user@example.org'); + + // Should reject invalid emails even if they end with '@example.com' + AssertValidateIsFalse('invalid@example.com@example.com'); + AssertValidateIsFalse('@example.com'); + }); + }); + + // Edge cases + describe('Edge Cases', () => { + it('Should handle empty string checks correctly', () => { + const schema = TypeBox(z.string().startsWith('').endsWith('')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Empty strings at start/end should match any string + AssertValidateIsTrue('anystring'); + AssertValidateIsTrue(''); + }); + ideally('Should handle special regex characters in string checks', () => { + const schema = TypeBox(z.string().startsWith('$^.+*?()[]{}|\\')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should properly escape regex special chars + AssertValidateIsTrue('$^.+*?()[]{}|\\followed'); + AssertValidateIsFalse('normal'); + }); + ideally('Should handle conflicting constraints', () => { + // This tests what happens with mutually exclusive constraints + const schema = TypeBox(z.string().startsWith('abc').startsWith('def')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // A string can't start with both 'abc' and 'def', so validation should fail + AssertValidateIsFalse('abcdef'); + AssertValidateIsFalse('defabc'); + }); + }); + }); + + // ---------------------------------------------------------------- + // Format and Constraint Combinations + // ---------------------------------------------------------------- + describe('Format and Constraint Combinations', () => { + it('Should combine format with length constraints', () => { + const schema = TypeBox(z.string().email().min(10).max(30)); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Valid email within length constraints + AssertValidateIsTrue('test@example.com'); // 15 chars + + // Valid email too short + AssertValidateIsFalse('a@b.com'); // 7 chars + + // Valid email too long + AssertValidateIsFalse('verylongusername@verylongdomain.com'); // > 30 chars + + // Invalid email but correct length + AssertValidateIsFalse('notanemailbutlongenough'); // 24 chars + }); + + it('Should combine format with pattern constraints', () => { + // Test a domain-specific email format: only gmail addresses + const schema = TypeBox(z.string().email().endsWith('@gmail.com')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Valid gmail addresses + AssertValidateIsTrue('user@gmail.com'); + AssertValidateIsTrue('test.account@gmail.com'); + + // Valid email but not gmail + AssertValidateIsFalse('user@example.com'); + + // Invalid email with gmail ending + AssertValidateIsFalse('not-valid@example@gmail.com'); + }); + ideally('Should handle multiple constraints on formats', () => { + // IPv4 addresses in a specific range with specific constraints + const schema = TypeBox(z.string().ip('v4').startsWith('192.168.').includes('.100')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Valid IPv4 meeting both constraints + AssertValidateIsTrue('192.168.0.100'); + AssertValidateIsTrue('192.168.100.101'); + + // Valid IPv4 but missing one constraint + AssertValidateIsFalse('192.168.0.1'); // Missing .100 + AssertValidateIsFalse('10.0.0.100'); // Has .100 but wrong prefix + + // Invalid IPv4 + AssertValidateIsFalse('192.168.300.100'); // Invalid IPv4 format + }); + }); +}); }) diff --git a/test/typebox-from-zod4.ts b/test/typebox-from-zod4.ts new file mode 100644 index 0000000..76a3f80 --- /dev/null +++ b/test/typebox-from-zod4.ts @@ -0,0 +1,507 @@ +import { TypeBox, Compile } from '@sinclair/typemap' +import { TSchema, TString, TypeGuard } from '@sinclair/typebox' +import { Assert } from './assert' +import { z } from 'zod/v4' +const ideally = process.env.EXTRA_TESTING === 'true' ? it : it.skip +describe('TypeBox From Zod4', () => { + // ---------------------------------------------------------------- + // Metadata + // ---------------------------------------------------------------- + it('Should map Description', () => { + const T = TypeBox(z.number().describe('a number')) + Assert.IsEqual(T.description, 'a number') + }) + + // ---------------------------------------------------------------- + // Any + // ---------------------------------------------------------------- + it('Should map Any', () => { + const T = TypeBox(z.any()) + Assert.IsTrue(TypeGuard.IsAny(T)) + }) + + // ---------------------------------------------------------------- + // Array + // ---------------------------------------------------------------- + it('Should map Array', () => { + const T = TypeBox(z.array(z.number())) + Assert.IsTrue(TypeGuard.IsArray(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.items)) + }) + + // ---------------------------------------------------------------- + // BigInt + // ---------------------------------------------------------------- + it('Should map BigInt', () => { + const T = TypeBox(z.bigint()) + Assert.IsTrue(TypeGuard.IsBigInt(T)) + }) + + // ---------------------------------------------------------------- + // Boolean + // ---------------------------------------------------------------- + it('Should map Boolean', () => { + const T = TypeBox(z.boolean()) + Assert.IsTrue(TypeGuard.IsBoolean(T)) + }) + + // ---------------------------------------------------------------- + // Number + // ---------------------------------------------------------------- + it('Should map Number', () => { + const T = TypeBox(z.number()) + Assert.IsTrue(TypeGuard.IsNumber(T)) + }) + + // ---------------------------------------------------------------- + // Object + // ---------------------------------------------------------------- + it('Should map Object', () => { + const T = TypeBox(z.object({ x: z.number() })) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsNumber(T.properties.x)) + }) + + // ---------------------------------------------------------------- + // String + // ---------------------------------------------------------------- + it('Should map String', () => { + const T = TypeBox(z.string()) + Assert.IsTrue(TypeGuard.IsString(T)) + }) + + // ---------------------------------------------------------------- + // String Formats + // ---------------------------------------------------------------- + it('Should map String Email format', () => { + const T = TypeBox(z.email()) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'email') + }) + + it('Should map String UUID format', () => { + const T = TypeBox(z.uuid()) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'uuid') + }) + + it('Should map String URL format', () => { + const T = TypeBox(z.url()) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'url') + }) + + it('Should map String IPv4 format', () => { + const T = TypeBox(z.ipv4()) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'ipv4') + }) + + it('Should map String IPv6 format', () => { + const T = TypeBox(z.ipv6()) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'ipv6') + }) + + it('Should map String CUID format', () => { + const T = TypeBox(z.cuid()) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'cuid') + }) + + it('Should map String CUID2 format', () => { + const T = TypeBox(z.cuid2()) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'cuid2') + }) + + it('Should map String ULID format', () => { + const T = TypeBox(z.ulid()) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'ulid') + }) + + it('Should map String Base64 format', () => { + const T = TypeBox(z.base64()) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'base64') + }) + + it('Should map String Base64URL format', () => { + const T = TypeBox(z.base64url()) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'base64url') + }) + + // ISO date formats + it('Should map String ISO Date format', () => { + const T = TypeBox(z.iso.date()) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'date') + }) + + it('Should map String ISO DateTime format', () => { + const T = TypeBox(z.iso.datetime()) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'datetime') + }) + + // ---------------------------------------------------------------- + // Edge Cases & Complex Cases + // ---------------------------------------------------------------- + it('Should map string format validators with transform', () => { + const T = TypeBox(z.email().transform(val => val.toLowerCase())) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'email') + }) + + it('Should map string format validators with pipe', () => { + const T = TypeBox(z.string().pipe(z.email())) + Assert.IsTrue(TypeGuard.IsString(T)) + // This might actually fail depending on how pipe is implemented in Zod v4 + }) + + it('Should map optional string format validators', () => { + const T = TypeBox(z.email().optional()) + Assert.IsTrue(TypeGuard.IsOptional(T)) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.format, 'email') + }) + + it('Should map array of string format validators', () => { + const T = TypeBox(z.array(z.email())) + Assert.IsTrue(TypeGuard.IsArray(T)) + Assert.IsTrue(TypeGuard.IsString(T.items)) + Assert.IsEqual(T.items.format, 'email') + }) + + it('Should map object with string format validators', () => { + const T = TypeBox(z.object({ + email: z.email(), + uuid: z.uuid(), + url: z.url() + })) + Assert.IsTrue(TypeGuard.IsObject(T)) + Assert.IsTrue(TypeGuard.IsString(T.properties.email)) + Assert.IsEqual(T.properties.email.format, 'email') + Assert.IsTrue(TypeGuard.IsString(T.properties.uuid)) + Assert.IsEqual(T.properties.uuid.format, 'uuid') + Assert.IsTrue(TypeGuard.IsString(T.properties.url)) + Assert.IsEqual(T.properties.url.format, 'url') + }) + it("Should respect string validator chaining",()=>{ + const T = TypeBox(z.email().min(5).max(10)) + Assert.IsTrue(TypeGuard.IsString(T)) + Assert.IsEqual(T.minLength, 5) + Assert.IsEqual(T.maxLength, 10) + Assert.IsEqual(T.format, 'email') + }) +}) + +// ---------------------------------------------------------------- +// String Validation Behavior Tests +// ---------------------------------------------------------------- +const Validator = (s: TSchema)=>{ + const S = Compile(s); + return { + AssertValidateIsTrue: (v: string) => { + const result = S.Check(v); + const err = result ? "is-valid" : `Validation was false (expected true) for value: '${v}' with schema:${JSON.stringify(s , null, 2)}`; + Assert.IsEqual(err, "is-valid"); + }, + AssertValidateIsFalse: (v: string) => { + const result = S.Check(v); + const err = !result ? "not-valid" : `Validation was true (expected false) for value: '${v}' with schema:${JSON.stringify(s , null, 2)}`; + Assert.IsEqual(err, "not-valid"); + } + } +} +describe('String Validation Behavior', () => { + + + // Tests for single string checks + describe('Basic String Checks', () => { + it('Should validate startsWith correctly', () => { + const schema = TypeBox(z.string().startsWith('prefix')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that start with 'prefix' + AssertValidateIsTrue('prefix123'); + AssertValidateIsTrue('prefixABC'); + + // Should reject strings that don't start with 'prefix' + AssertValidateIsFalse('wrongprefix'); + AssertValidateIsFalse('wrong'); + }); + + it('Should validate endsWith correctly', () => { + const schema = TypeBox(z.string().endsWith('suffix')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that end with 'suffix' + AssertValidateIsTrue('123suffix'); + AssertValidateIsTrue('ABCsuffix'); + + // Should reject strings that don't end with 'suffix' + AssertValidateIsFalse('suffixwrong'); + AssertValidateIsFalse('wrong'); + }); + + ideally('Should validate includes correctly', () => { + const schema = TypeBox(z.string().includes('middle')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that include 'middle' + AssertValidateIsTrue('start-middle-end'); + AssertValidateIsTrue('middleAtStart'); + AssertValidateIsTrue('atTheMiddle'); + + // Should reject strings that don't include 'middle' + AssertValidateIsFalse('nomid'); + AssertValidateIsFalse('midle'); // Misspelled + }); + + ideally('Should validate regex patterns correctly', () => { + const schema = TypeBox(z.string().regex(/^A[BC]+D$/)); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that match the regex + AssertValidateIsTrue('ABCD'); + AssertValidateIsTrue('AABBBCD'); + AssertValidateIsTrue('AABCD'); + AssertValidateIsTrue('AABBBBCD'); + // Should reject strings that don't match the regex + AssertValidateIsFalse('AC'); + AssertValidateIsFalse('123'); + AssertValidateIsFalse('mixed123'); + }); + + it('Should validate lowercase correctly', () => { + const schema = TypeBox(z.string().lowercase()); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept lowercase strings + AssertValidateIsTrue('abc'); + AssertValidateIsTrue('lowercase'); + + // Should reject strings with uppercase + AssertValidateIsFalse('Abc'); + AssertValidateIsFalse('UPPERCASE'); + AssertValidateIsFalse('mixedCASE'); + }); + + it('Should validate uppercase correctly', () => { + const schema = TypeBox(z.string().uppercase()); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept uppercase strings + AssertValidateIsTrue('ABC'); + AssertValidateIsTrue('UPPERCASE'); + + // Should reject strings with lowercase + AssertValidateIsFalse('aBC'); + AssertValidateIsFalse('lowercase'); + AssertValidateIsFalse('mixedCASE'); + }); + }); + + // Tests for combined string checks + describe('Combined String Checks', () => { + it('Should validate startsWith + endsWith correctly', () => { + const schema = TypeBox(z.string().startsWith('start').endsWith('end')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that start with 'start' AND end with 'end' + AssertValidateIsTrue('startend'); + AssertValidateIsTrue('start_middle_end'); + + // Should reject strings that don't meet both conditions + AssertValidateIsFalse('startonly'); + AssertValidateIsFalse('endonly'); + AssertValidateIsFalse('neither'); + }); + + it('Should validate startsWith + includes correctly', () => { + const schema = TypeBox(z.string().startsWith('start').includes('middle')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that start with 'start' AND include 'middle' + AssertValidateIsTrue('startmiddle'); + AssertValidateIsTrue('start_middle_end'); + + // Should reject strings that don't meet both conditions + AssertValidateIsFalse('startonly'); + AssertValidateIsFalse('middleonly'); + AssertValidateIsFalse('neither'); + }); + + it('Should validate endsWith + includes correctly', () => { + const schema = TypeBox(z.string().endsWith('end').includes('middle')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that end with 'end' AND include 'middle' + AssertValidateIsTrue('middleend'); + AssertValidateIsTrue('start_middle_end'); + + // Should reject strings that don't meet both conditions + AssertValidateIsFalse('endonly'); + AssertValidateIsFalse('middleonly'); + AssertValidateIsFalse('neither'); + }); + + it('Should validate startsWith + endsWith + includes correctly', () => { + const schema = TypeBox(z.string().startsWith('start').endsWith('end').includes('middle')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that meet all three conditions + AssertValidateIsTrue('startmiddleend'); + AssertValidateIsTrue('start_middle_end'); + + // Should reject strings that don't meet all conditions + AssertValidateIsFalse('startend'); // Missing 'middle' + AssertValidateIsFalse('startmiddle'); // Missing 'end' + AssertValidateIsFalse('middlewithend'); // Missing 'start' + AssertValidateIsFalse('neither'); + }); + }); + + // Tests for special cases mentioned in the comments + describe('Special Case Combinations', () => { + it('Should validate ipv4 with startsWith correctly', () => { + // This tests the commented case "z.ipv4().startsWith('192.168.')" + const schema = TypeBox(z.ipv4().startsWith('192.168.')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept IPv4 addresses that start with '192.168.' + AssertValidateIsTrue('192.168.1.1'); + AssertValidateIsTrue('192.168.0.1'); + AssertValidateIsTrue('192.168.254.254'); + + // Should reject IPv4 addresses that don't start with '192.168.' + AssertValidateIsFalse('10.0.0.1'); + AssertValidateIsFalse('172.16.0.1'); + + // Should reject strings that start with '192.168.' but aren't valid IPv4s + AssertValidateIsFalse('192.168.'); + AssertValidateIsFalse('192.168.1'); + AssertValidateIsFalse('192.168.1.'); + AssertValidateIsFalse('192.168.256.1'); + }); + + ideally('Should validate regex with startsWith correctly', () => { + const schema = TypeBox(z.string().regex(/^[a-z]+$/).startsWith('abc')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept strings that are lowercase AND start with 'abc' + AssertValidateIsTrue('abc'); + AssertValidateIsTrue('abcdef'); + + // Should reject strings that don't meet both conditions + AssertValidateIsFalse('abcDEF'); // Not all lowercase + AssertValidateIsFalse('def'); // Doesn't start with 'abc' + }); + + it('Should validate email with endsWith correctly', () => { + const schema = TypeBox(z.email().endsWith('@example.com')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should accept emails that end with '@example.com' + AssertValidateIsTrue('user@example.com'); + AssertValidateIsTrue('another.user@example.com'); + + // Should reject emails that don't end with '@example.com' + AssertValidateIsFalse('user@gmail.com'); + AssertValidateIsFalse('user@example.org'); + + // Should reject invalid emails even if they end with '@example.com' + AssertValidateIsFalse('invalid@example.com@example.com'); + AssertValidateIsFalse('@example.com'); + }); + }); + + // Edge cases + describe('Edge Cases', () => { + it('Should handle empty string checks correctly', () => { + const schema = TypeBox(z.string().startsWith('').endsWith('')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Empty strings at start/end should match any string + AssertValidateIsTrue('anystring'); + AssertValidateIsTrue(''); + }); + + it('Should handle special regex characters in string checks', () => { + const schema = TypeBox(z.string().startsWith('$^.+*?()[]{}|\\')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Should properly escape regex special chars + AssertValidateIsTrue('$^.+*?()[]{}|\\followed'); + AssertValidateIsFalse('normal'); + }); + + it('Should handle conflicting constraints', () => { + // This tests what happens with mutually exclusive constraints + const schema = TypeBox(z.string().startsWith('abc').startsWith('def')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // A string can't start with both 'abc' and 'def', so validation should fail + AssertValidateIsFalse('abcdef'); + AssertValidateIsFalse('defabc'); + }); + }); +}); + +// ---------------------------------------------------------------- +// Format and Constraint Combinations +// ---------------------------------------------------------------- +describe('Format and Constraint Combinations', () => { + it('Should combine format with length constraints', () => { + const schema = TypeBox(z.email().min(10).max(30)); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Valid email within length constraints + AssertValidateIsTrue('test@example.com'); // 15 chars + + // Valid email too short + AssertValidateIsFalse('a@b.com'); // 7 chars + + // Valid email too long + AssertValidateIsFalse('verylongusername@verylongdomain.com'); // > 30 chars + + // Invalid email but correct length + AssertValidateIsFalse('notanemailbutlongenough'); // 24 chars + }); + + it('Should combine format with pattern constraints', () => { + // Test a domain-specific email format: only gmail addresses + const schema = TypeBox(z.email().endsWith('@gmail.com')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Valid gmail addresses + AssertValidateIsTrue('user@gmail.com'); + AssertValidateIsTrue('test.account@gmail.com'); + + // Valid email but not gmail + AssertValidateIsFalse('user@example.com'); + + // Invalid email with gmail ending + AssertValidateIsFalse('not-valid@example@gmail.com'); + }); + + it('Should handle multiple constraints on formats', () => { + // IPv4 addresses in a specific range with specific constraints + const schema = TypeBox(z.ipv4().startsWith('192.168.').includes('.100')); + const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema); + + // Valid IPv4 meeting both constraints + AssertValidateIsTrue('192.168.0.100'); + AssertValidateIsTrue('192.168.100.101'); + + // Valid IPv4 but missing one constraint + AssertValidateIsFalse('192.168.0.1'); // Missing .100 + AssertValidateIsFalse('10.0.0.100'); // Has .100 but wrong prefix + + // Invalid IPv4 + AssertValidateIsFalse('192.168.300.100'); // Invalid IPv4 format + }); +}); diff --git a/test/zod-detection-test.ts b/test/zod-detection-test.ts new file mode 100644 index 0000000..408a219 --- /dev/null +++ b/test/zod-detection-test.ts @@ -0,0 +1,24 @@ +import * as z3 from 'zod'; +import { z as z4 } from 'zod/v4'; +import { IsZod, IsZod4 } from '../src/guard'; +import { Assert } from './assert'; + +describe('Zod Version Detection', () => { + // Create test schemas + const z3String = z3.string(); + const z4String = z4.string(); + + describe('Zod v3 Detection', () => { + it('Should correctly identify Zod v3 schema', () => { + Assert.IsTrue(IsZod(z3String)); + Assert.IsFalse(IsZod4(z3String)); + }); + }); + + describe('Zod v4 Detection', () => { + it('Should correctly identify Zod v4 schema', () => { + Assert.IsFalse(IsZod(z4String)); + Assert.IsTrue(IsZod4(z4String)); + }); + }); +}); diff --git a/test/zod4-from-typebox.ts b/test/zod4-from-typebox.ts new file mode 100644 index 0000000..55e3ec3 --- /dev/null +++ b/test/zod4-from-typebox.ts @@ -0,0 +1,61 @@ +import { Zod4 } from '@sinclair/typemap' +import { Assert } from './assert' +import * as t from '@sinclair/typebox' +import { z } from 'zod/v4' + +describe('Zod4 From TypeBox', () => { + // ---------------------------------------------------------------- + // Any + // ---------------------------------------------------------------- + it('Should map Any', () => { + const Z = Zod4(t.Any()) + Assert.IsTrue(Z.safeParse(1).success) + Assert.IsTrue(Z.safeParse('hello').success) + Assert.IsTrue(Z.safeParse(true).success) + }) + + // ---------------------------------------------------------------- + // Boolean + // ---------------------------------------------------------------- + it('Should map Boolean', () => { + const Z = Zod4(t.Boolean()) + Assert.IsTrue(Z.safeParse(true).success) + Assert.IsFalse(Z.safeParse(1).success) + }) + + // ---------------------------------------------------------------- + // Number + // ---------------------------------------------------------------- + it('Should map Number', () => { + const Z = Zod4(t.Number()) + Assert.IsTrue(Z.safeParse(1).success) + Assert.IsFalse(Z.safeParse('hello').success) + }) + + // ---------------------------------------------------------------- + // Object + // ---------------------------------------------------------------- + it('Should map Object', () => { + const Z = Zod4(t.Object({ x: t.Number(), y: t.String() })) + Assert.IsTrue(Z.safeParse({ x: 1, y: 'hello' }).success) + Assert.IsFalse(Z.safeParse({ x: 'hello', y: 1 }).success) + }) + + // ---------------------------------------------------------------- + // String + // ---------------------------------------------------------------- + it('Should map String', () => { + const Z = Zod4(t.String()) + Assert.IsTrue(Z.safeParse('hello').success) + Assert.IsFalse(Z.safeParse(1).success) + }) + + // ---------------------------------------------------------------- + // String Format + // ---------------------------------------------------------------- + it('Should map String with Email format', () => { + const Z = Zod4(t.String({ format: 'email' })) + Assert.IsTrue(Z.safeParse('test@example.com').success) + Assert.IsFalse(Z.safeParse('not-an-email').success) + }) +}) diff --git a/test/zod4-from-zod-and-zod-from-zod4.ts b/test/zod4-from-zod-and-zod-from-zod4.ts new file mode 100644 index 0000000..989b24d --- /dev/null +++ b/test/zod4-from-zod-and-zod-from-zod4.ts @@ -0,0 +1,82 @@ +import { Zod, Zod4 } from '@sinclair/typemap' +import { Assert } from './assert' +import * as z from 'zod' +import { z as z4 } from 'zod/v4' + +describe('Zod4 From Zod and Zod From Zod4', () => { + // ---------------------------------------------------------------- + // String Conversion + // ---------------------------------------------------------------- + it('Should convert Zod string to Zod4 string', () => { + const zodString = z.string() + const zod4String = Zod4(zodString) + + // Verify it's a Zod4 string type + Assert.IsTrue(typeof zod4String.parse === 'function') + Assert.IsEqual(zod4String.parse('test'), 'test') + }) + + it('Should convert Zod4 string to Zod string', () => { + const zod4String = z4.string() + const zodString = Zod(zod4String) + + // Verify it's a Zod string type + Assert.IsTrue(typeof zodString.parse === 'function') + Assert.IsEqual(zodString.parse('test'), 'test') + }) + + // ---------------------------------------------------------------- + // Object Conversion + // ---------------------------------------------------------------- + it('Should convert Zod object to Zod4 object', () => { + const zodObject = z.object({ + name: z.string(), + age: z.number() + }) + + const zod4Object = Zod4(zodObject) + + // Verify it's a Zod4 object type and can parse correctly + const testData = { name: 'John', age: 30 } + Assert.IsTrue(typeof zod4Object.parse === 'function') + const parsed = zod4Object.parse(testData) + Assert.IsEqual(parsed, testData) + }) + + it('Should convert Zod4 object to Zod object', () => { + const zod4Object = z4.object({ + name: z4.string(), + age: z4.number() + }) + + const zodObject = Zod(zod4Object) + + // Verify it's a Zod object type and can parse correctly + const testData = { name: 'John', age: 30 } + Assert.IsTrue(typeof zodObject.parse === 'function') + const parsed = zodObject.parse(testData) + Assert.IsEqual(parsed, testData) + }) + + // ---------------------------------------------------------------- + // Back and Forth Conversion + // ---------------------------------------------------------------- + it('Should preserve structure when converting Zod → Zod4 → Zod', () => { + const original = z.object({ + name: z.string(), + age: z.number(), + isAdmin: z.boolean().optional() + }) + + const toZod4 = Zod4(original) + const backToZod = Zod(toZod4) + + // Verify round-trip conversion works + const testData = { name: 'John', age: 30 } + Assert.IsEqual(backToZod.parse(testData), testData) + + // Optional field should work too + const testDataWithOptional = { name: 'John', age: 30, isAdmin: true } + Assert.IsEqual(backToZod.parse(testDataWithOptional), testDataWithOptional) + }) +}) diff --git a/test/zod4-tsc-tests.ts b/test/zod4-tsc-tests.ts new file mode 100644 index 0000000..0d7a6d2 --- /dev/null +++ b/test/zod4-tsc-tests.ts @@ -0,0 +1,625 @@ +import { TypeBoxFromZod4 as TypeBox } from '../src/typebox/typebox-from-zod4' +import * as t from '@sinclair/typebox' +import { z } from 'zod/v4' + +// Ensure that the TS types are assignable to what we expect +type AssertEquivalence = T extends U ? (U extends T ? V : never) : never; + +// ------------------------------------------------------------------------ +// ZodAny +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const anySchema = z.any(); +// 2. Manually define the expected output type +type AnyType = t.TAny; +// 3. Convert the Zod Schema into TypeBox schema +const anyTypeBox = TypeBox(anySchema); +// 4. Infer the type of the TypeBox type +type AnyTypeBoxType = t.Static; +// 5. Create a TS assertion +const anyTypeAssertion: [ + AssertEquivalence +] = ["AnyTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodArray +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const arraySchema = z.array(z.string()); +// 2. Manually define the expected output type +type ArrayType = string[]; +// 3. Convert the Zod Schema into TypeBox schema +const arrayTypeBox = TypeBox(arraySchema); +// 4. Infer the type of the TypeBox type +type ArrayTypeBoxType = t.Static; +// 5. Create a TS assertion +const arrayTypeAssertion: [ + AssertEquivalence +] = ["ArrayTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodBigInt +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const bigIntSchema = z.bigint(); +// 2. Manually define the expected output type +type BigIntType = bigint; +// 3. Convert the Zod Schema into TypeBox schema +const bigIntTypeBox = TypeBox(bigIntSchema); +// 4. Infer the type of the TypeBox type +type BigIntTypeBoxType = t.Static; +// 5. Create a TS assertion +const bigIntTypeAssertion: [ + AssertEquivalence +] = ["BigIntTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodBoolean +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const booleanSchema = z.boolean(); +// 2. Manually define the expected output type +type BooleanType = boolean; +// 3. Convert the Zod Schema into TypeBox schema +const booleanTypeBox = TypeBox(booleanSchema); +// 4. Infer the type of the TypeBox type +type BooleanTypeBoxType = t.Static; +// 5. Create a TS assertion +const booleanTypeAssertion: [ + AssertEquivalence +] = ["BooleanTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodCatch +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const catchSchema = z.string().catch("default"); +// 2. Manually define the expected output type +type CatchType = string; +// 3. Convert the Zod Schema into TypeBox schema +const catchTypeBox = TypeBox(catchSchema); +// 4. Infer the type of the TypeBox type +type CatchTypeBoxType = t.Static; +// 5. Create a TS assertion +const catchTypeAssertion: [ + AssertEquivalence +] = ["CatchTypeAssertion"]; + +// // ------------------------------------------------------------------------ +// // ZodCustom - No idea how to handle this in TypeBox +// // ------------------------------------------------------------------------ +// // 1. Define a schema in Zod v4 +// const customSchema = z.custom(val => typeof val === 'string'); +// // 2. Manually define the expected output type +// type CustomType = string; +// // 3. Convert the Zod Schema into TypeBox schema +// const customTypeBox = TypeBox(customSchema); +// // 4. Infer the type of the TypeBox type +// type CustomTypeBoxType = t.Static; +// // 5. Create a TS assertion +// const customTypeAssertion: [ +// AssertEquivalence +// ] = ["CustomTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodDate +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const dateSchema = z.date(); +// 2. Manually define the expected output type +type DateType = Date; +// 3. Convert the Zod Schema into TypeBox schema +const dateTypeBox = TypeBox(dateSchema); +// 4. Infer the type of the TypeBox type +type DateTypeBoxType = t.Static; +// 5. Create a TS assertion +const dateTypeAssertion: [ + AssertEquivalence +] = ["DateTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodDefault +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const defaultSchema = z.string().default("default"); +// 2. Manually define the expected output type +type DefaultType = string; +// 3. Convert the Zod Schema into TypeBox schema +const defaultTypeBox = TypeBox(defaultSchema); +// 4. Infer the type of the TypeBox type +type DefaultTypeBoxType = t.Static; +// 5. Create a TS assertion +const defaultTypeAssertion: [ + AssertEquivalence +] = ["DefaultTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodDiscriminatedUnion +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const dogSchema = z.object({ + type: z.literal('dog'), + bark: z.string() +}); +const catSchema = z.object({ + type: z.literal('cat'), + meow: z.string() +}); +const discriminatedUnionSchema = z.discriminatedUnion('type', [dogSchema, catSchema]); +// 2. Manually define the expected output type +type DiscriminatedUnionType = { + type: 'dog'; + bark: string; +} | { + type: 'cat'; + meow: string; +}; +// 3. Convert the Zod Schema into TypeBox schema +const discriminatedUnionTypeBox = TypeBox(discriminatedUnionSchema); +// 4. Infer the type of the TypeBox type +type DiscriminatedUnionTypeBoxType = t.Static; +// 5. Create a TS assertion +const discriminatedUnionTypeAssertion: [ + AssertEquivalence +] = ["DiscriminatedUnionTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodEnum +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const enumSchema = z.enum(['a', 'b', 'c']); +// 2. Manually define the expected output type +type EnumType = 'a' | 'b' | 'c'; +// 3. Convert the Zod Schema into TypeBox schema +const enumTypeBox = TypeBox(enumSchema); +// 4. Infer the type of the TypeBox type +type EnumTypeBoxType = t.Static; +// 5. Create a TS assertion +const enumTypeAssertion: [ + AssertEquivalence +] = ["EnumTypeAssertion"]; + +// // ------------------------------------------------------------------------ +// // ZodLazy - No idea - could use some expert help here +// // ------------------------------------------------------------------------ +// // 1. Define a schema in Zod v4 +// type TreeNode = { value: string; children: TreeNode[] }; +// const lazySchema: z.ZodType = z.lazy(() => +// z.object({ +// value: z.string(), +// children: z.array(lazySchema) +// }) +// ); +// // 2. Manually define the expected output type +// type LazyType = TreeNode; +// // 3. Convert the Zod Schema into TypeBox schema +// const lazyTypeBox = TypeBox(lazySchema); +// // 4. Infer the type of the TypeBox type +// type LazyTypeBoxType = t.Static; +// // 5. Create a TS assertion +// const lazyTypeAssertion: [ +// AssertEquivalence +// ] = ["LazyTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodLiteral +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const literalSchema = z.literal('hello'); +// 2. Manually define the expected output type +type LiteralType = 'hello'; +// 3. Convert the Zod Schema into TypeBox schema +const literalTypeBox = TypeBox(literalSchema); +// 4. Infer the type of the TypeBox type +type LiteralTypeBoxType = t.Static; +// 5. Create a TS assertion +const literalTypeAssertion: [ + AssertEquivalence +] = ["LiteralTypeAssertion"]; + +// // ------------------------------------------------------------------------ +// // ZodMap - No such TypeBox type exists +// // ------------------------------------------------------------------------ +// // 1. Define a schema in Zod v4 +// const mapSchema = z.map(z.string(), z.number()); +// // 2. Manually define the expected output type +// type MapType = Map; +// // 3. Convert the Zod Schema into TypeBox schema +// const mapTypeBox = TypeBox(mapSchema); +// // 4. Infer the type of the TypeBox type +// type MapTypeBoxType = t.Static; +// // 5. Create a TS assertion +// const mapTypeAssertion: [ +// AssertEquivalence +// ] = ["MapTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodNever +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const neverSchema = z.never(); +// 2. Manually define the expected output type +type NeverType = never; +// 3. Convert the Zod Schema into TypeBox schema +const neverTypeBox = TypeBox(neverSchema); +// 4. Infer the type of the TypeBox type +type NeverTypeBoxType = t.Static; +// 5. Create a TS assertion +// @ts-expect- +const neverTypeAssertion: [ + AssertEquivalence +] = ["NeverTypeAssertion"] as never; + +// ------------------------------------------------------------------------ +// ZodNull +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const nullSchema = z.null(); +// 2. Manually define the expected output type +type NullType = null; +// 3. Convert the Zod Schema into TypeBox schema +const nullTypeBox = TypeBox(nullSchema); +// 4. Infer the type of the TypeBox type +type NullTypeBoxType = t.Static; +// 5. Create a TS assertion +const nullTypeAssertion: [ + AssertEquivalence +] = ["NullTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodNullable +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const nullableSchema = z.string().nullable(); +// 2. Manually define the expected output type +type NullableType = string | null; +// 3. Convert the Zod Schema into TypeBox schema +const nullableTypeBox = TypeBox(nullableSchema); +// 4. Infer the type of the TypeBox type +type NullableTypeBoxType = t.Static; +// 5. Create a TS assertion +const nullableTypeAssertion: [ + AssertEquivalence +] = ["NullableTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodNumber +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const numberSchema = z.number(); +// 2. Manually define the expected output type +type NumberType = number; +// 3. Convert the Zod Schema into TypeBox schema +const numberTypeBox = TypeBox(numberSchema); +// 4. Infer the type of the TypeBox type +type NumberTypeBoxType = t.Static; +// 5. Create a TS assertion +const numberTypeAssertion: [ + AssertEquivalence +] = ["NumberTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodObject +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const objectSchema = z.object({ + name: z.string(), + age: z.number(), +}); +// 2. Manually define the expected output type +type ObjectType = { + name: string; + age: number; +}; +// 3. Convert the Zod Schema into TypeBox schema +const objectTypeBox = TypeBox(objectSchema); +// 4. Infer the type of the TypeBox type +type ObjectTypeBoxType = t.Static; +// 5. Create a TS assertion +const objectTypeAssertion: [ + AssertEquivalence +] = ["ObjectTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodOptional +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const optionalSchema = z.string().optional(); +// 2. Manually define the expected output type +type OptionalType = string | undefined; +// 3. Convert the Zod Schema into TypeBox schema +const optionalTypeBox = TypeBox(optionalSchema); +// 4. Infer the type of the TypeBox type +type OptionalTypeBoxType = t.Static; +// 5. Create a TS assertion +const optionalTypeAssertion: [ + AssertEquivalence +] = ["OptionalTypeAssertion"]; + +// // ------------------------------------------------------------------------ +// // ZodPipe - Too complicate, never seen it used +// // ------------------------------------------------------------------------ +// // 1. Define a schema in Zod v4 +// const pipeSchema = z.string().pipe(z.number()); +// // 2. Manually define the expected output type +// type PipeType = number; +// // 3. Convert the Zod Schema into TypeBox schema +// const pipeTypeBox = TypeBox(pipeSchema); +// // 4. Infer the type of the TypeBox type +// type PipeTypeBoxType = t.Static; +// // 5. Create a TS assertion +// const pipeTypeAssertion: [ +// AssertEquivalence +// ] = ["PipeTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodPromise - Not directly supported in TypeBox +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const promiseSchema = z.promise(z.string()); +// 2. Manually define the expected output type +type PromiseType = Promise; +// 3. Convert the Zod Schema into TypeBox schema +const promiseTypeBox = TypeBox(promiseSchema); +// 4. Infer the type of the TypeBox type +type PromiseTypeBoxType = t.Static; +// 5. Create a TS assertion +const promiseTypeAssertion: [ + AssertEquivalence +] = ["PromiseTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodRecord +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const recordSchema = z.record(z.string(), z.number()); +// 2. Manually define the expected output type +type RecordType = Record; +// 3. Convert the Zod Schema into TypeBox schema +const recordTypeBox = TypeBox(recordSchema); +// 4. Infer the type of the TypeBox type +type RecordTypeBoxType = t.Static; +// 5. Create a TS assertion +const recordTypeAssertion: [ + AssertEquivalence +] = ["RecordTypeAssertion"]; + +// // ------------------------------------------------------------------------ +// // ZodSet - Like Map, no such TypeBox type exists +// // ------------------------------------------------------------------------ +// // 1. Define a schema in Zod v4 +// const setSchema = z.set(z.string()); +// // 2. Manually define the expected output type +// type SetType = Set; +// // 3. Convert the Zod Schema into TypeBox schema +// const setTypeBox = TypeBox(setSchema); +// // 4. Infer the type of the TypeBox type +// type SetTypeBoxType = t.Static; +// // 5. Create a TS assertion +// const setTypeAssertion: [ +// AssertEquivalence +// ] = ["SetTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodString +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const stringSchema = z.string(); +// 2. Manually define the expected output type +type StringType = string; +// 3. Convert the Zod Schema into TypeBox schema +const stringTypeBox = TypeBox(stringSchema); +// 4. Infer the type of the TypeBox type +type StringTypeBoxType = t.Static; +// 5. Create a TS assertion +const stringTypeAssertion: [ + AssertEquivalence +] = ["StringTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodSymbol +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const symbolSchema = z.symbol(); +// 2. Manually define the expected output type +type SymbolType = symbol; +// 3. Convert the Zod Schema into TypeBox schema +const symbolTypeBox = TypeBox(symbolSchema); +// 4. Infer the type of the TypeBox type +type SymbolTypeBoxType = t.Static; +// 5. Create a TS assertion +const symbolTypeAssertion: [ + AssertEquivalence +] = ["SymbolTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodTemplateLiteral -- SO CLOSE - need help! +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const templateLiteralSchema = z.templateLiteral(['Hello, ', z.string()]); +// 2. Manually define the expected output type +type TemplateLiteralType = `Hello, ${string}`; +// 3. Convert the Zod Schema into TypeBox schema +const templateLiteralTypeBox = TypeBox(templateLiteralSchema); +// 4. Infer the type of the TypeBox type +type TemplateLiteralTypeBoxType = t.Static; +// 5. Create a TS assertion +// const templateLiteralTypeAssertion: [ +// AssertEquivalence +// ] = ["TemplateLiteralTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodTuple +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const tupleSchema = z.tuple([z.string(), z.number(), z.boolean()]); +// 2. Manually define the expected output type +type TupleType = [string, number, boolean]; +// 3. Convert the Zod Schema into TypeBox schema +const tupleTypeBox = TypeBox(tupleSchema); +// 4. Infer the type of the TypeBox type +type TupleTypeBoxType = t.Static; +// 5. Create a TS assertion +const tupleTypeAssertion: [ + AssertEquivalence +] = ["TupleTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodUndefined +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const undefinedSchema = z.undefined(); +// 2. Manually define the expected output type +type UndefinedType = undefined; +// 3. Convert the Zod Schema into TypeBox schema +const undefinedTypeBox = TypeBox(undefinedSchema); +// 4. Infer the type of the TypeBox type +type UndefinedTypeBoxType = t.Static; +// 5. Create a TS assertion +const undefinedTypeAssertion: [ + AssertEquivalence +] = ["UndefinedTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodUnion +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const unionSchema = z.union([z.string(), z.number()]); +// 2. Manually define the expected output type +type UnionType = string | number; +// 3. Convert the Zod Schema into TypeBox schema +const unionTypeBox = TypeBox(unionSchema); +// 4. Infer the type of the TypeBox type +type UnionTypeBoxType = t.Static; +// 5. Create a TS assertion +const unionTypeAssertion: [ + AssertEquivalence +] = ["UnionTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodUnknown +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const unknownSchema = z.unknown(); +// 2. Manually define the expected output type +type UnknownType = unknown; +// 3. Convert the Zod Schema into TypeBox schema +const unknownTypeBox = TypeBox(unknownSchema); +// 4. Infer the type of the TypeBox type +type UnknownTypeBoxType = t.Static; +// 5. Create a TS assertion +const unknownTypeAssertion: [ + AssertEquivalence +] = ["UnknownTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodVoid +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const voidSchema = z.void(); +// 2. Manually define the expected output type +type VoidType = void; +// 3. Convert the Zod Schema into TypeBox schema +const voidTypeBox = TypeBox(voidSchema); +// 4. Infer the type of the TypeBox type +type VoidTypeBoxType = t.Static; +// 5. Create a TS assertion +const voidTypeAssertion: [ + AssertEquivalence +] = ["VoidTypeAssertion"]; + +// ------------------------------------------------------------------------ +// ZodIntersection +// ------------------------------------------------------------------------ +// 1. Define a schema in Zod v4 +const aSchema = z.object({ a: z.string() }); +const bSchema = z.object({ b: z.number() }); +const intersectionSchema = aSchema.and(bSchema); +// 2. Manually define the expected output type +type IntersectionType = { + a: string; +} & { + b: number; +}; +// 3. Convert the Zod Schema into TypeBox schema +const intersectionTypeBox = TypeBox(intersectionSchema); +// 4. Infer the type of the TypeBox type +type IntersectionTypeBoxType = t.Static; +// 5. Create a TS assertion +const intersectionTypeAssertion: [ + AssertEquivalence +] = ["IntersectionTypeAssertion"]; + +// ------------------------------------------------------------------------ +// Complete Assertion Set +// ------------------------------------------------------------------------ +const assertEquivalence: [ + typeof anyTypeAssertion[0], + typeof arrayTypeAssertion[0], + typeof bigIntTypeAssertion[0], + typeof booleanTypeAssertion[0], + typeof catchTypeAssertion[0], +// typeof customTypeAssertion[0], + typeof dateTypeAssertion[0], + typeof defaultTypeAssertion[0], + typeof discriminatedUnionTypeAssertion[0], + typeof enumTypeAssertion[0], +// typeof lazyTypeAssertion[0], + typeof literalTypeAssertion[0], +// typeof mapTypeAssertion[0], +// typeof neverTypeAssertion[0], + typeof nullTypeAssertion[0], + typeof nullableTypeAssertion[0], + typeof numberTypeAssertion[0], + typeof objectTypeAssertion[0], + typeof optionalTypeAssertion[0], +// typeof pipeTypeAssertion[0], + typeof promiseTypeAssertion[0], + typeof recordTypeAssertion[0], +// typeof setTypeAssertion[0], + typeof stringTypeAssertion[0], + typeof symbolTypeAssertion[0], +// typeof templateLiteralTypeAssertion[0], // for now - so close! + typeof tupleTypeAssertion[0], + typeof undefinedTypeAssertion[0], + typeof unionTypeAssertion[0], + typeof unknownTypeAssertion[0], + typeof voidTypeAssertion[0], + typeof intersectionTypeAssertion[0] +] = [ + "AnyTypeAssertion", + "ArrayTypeAssertion", + "BigIntTypeAssertion", + "BooleanTypeAssertion", + "CatchTypeAssertion", +// "CustomTypeAssertion", + "DateTypeAssertion", + "DefaultTypeAssertion", + "DiscriminatedUnionTypeAssertion", + "EnumTypeAssertion", +// "LazyTypeAssertion", + "LiteralTypeAssertion", +// "MapTypeAssertion", +// "NeverTypeAssertion", + "NullTypeAssertion", + "NullableTypeAssertion", + "NumberTypeAssertion", + "ObjectTypeAssertion", + "OptionalTypeAssertion", +// "PipeTypeAssertion", + "PromiseTypeAssertion", + "RecordTypeAssertion", +// "SetTypeAssertion", + "StringTypeAssertion", + "SymbolTypeAssertion", +// "TemplateLiteralTypeAssertion", // for now - so close! + "TupleTypeAssertion", + "UndefinedTypeAssertion", + "UnionTypeAssertion", + "UnknownTypeAssertion", + "VoidTypeAssertion", + "IntersectionTypeAssertion" +]; + +// All assertions should pass, otherwise TypeScript will throw an error +if (assertEquivalence.some(x => !x)) throw new Error("Type assertion failed");