├── .md ├── .github └── FUNDING.yml ├── bun.lockb ├── src ├── index.ts ├── zu.ts ├── lib │ ├── pick.ts │ ├── omit.ts │ ├── omit.test.ts │ ├── pick.test.ts │ └── mapValues.ts ├── SPR.test.ts ├── stringToJSON.ts ├── SPR.ts ├── json.ts ├── json.test.ts ├── useTypedParsers.ts ├── stringToJSON.test.ts ├── partialSafeParse.ts ├── useTypedParsers.test.ts ├── makeErrorMap.ts ├── useFormLike.ts ├── partialSafeParse.test.ts ├── coerce.ts ├── makeErrorMap.test.ts ├── useFormLike.test.ts └── coerce.test.ts ├── tsconfig.json ├── scripts ├── publish.ts └── build.ts ├── package.json ├── LICENSE ├── .gitignore ├── README.md └── logo.svg /.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ```ts 4 | ``` -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: JacobWeisenburger 2 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobWeisenburger/zod_utilz/HEAD/bun.lockb -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './zu' 2 | export * as zu from './zu' 3 | import * as zu from './zu' 4 | export default zu -------------------------------------------------------------------------------- /src/zu.ts: -------------------------------------------------------------------------------- 1 | export * from './SPR' 2 | export * from './makeErrorMap' 3 | export * from './useTypedParsers' 4 | export * from './coerce' 5 | export * from './json' 6 | export * from './stringToJSON' 7 | export * from './useFormLike' 8 | export * from './partialSafeParse' -------------------------------------------------------------------------------- /src/lib/pick.ts: -------------------------------------------------------------------------------- 1 | export const pick = > 2 | ( obj: Obj, keys: ( keyof Obj )[] ) => { 3 | if ( keys.length === 0 ) return {} 4 | const entries = Object.entries( obj ) as [ keyof Obj, unknown ][] 5 | return Object.fromEntries( 6 | entries.filter( ( [ key ] ) => keys.includes( key ) ) 7 | ) 8 | } -------------------------------------------------------------------------------- /src/lib/omit.ts: -------------------------------------------------------------------------------- 1 | export const omit = > 2 | ( obj: Obj, keys: ( keyof Obj | string )[] ) => { 3 | if ( keys.length === 0 ) return obj 4 | const entries = Object.entries( obj ) as [ keyof Obj, unknown ][] 5 | return Object.fromEntries( 6 | entries.filter( ( [ key ] ) => !keys.includes( key ) ) 7 | ) 8 | } -------------------------------------------------------------------------------- /src/lib/omit.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { omit } from './omit' 3 | 4 | const obj = { 5 | foo: 'foo', 6 | bar: 42, 7 | baz: true, 8 | } 9 | 10 | test( 'omit', () => { 11 | expect( omit( obj, [] ) ).toMatchObject( obj ) 12 | expect( omit( obj, [ 'foo' ] ) ).toMatchObject( { bar: 42, baz: true } ) 13 | expect( omit( obj, [ 'foo', 'bar' ] ) ).toMatchObject( { baz: true } ) 14 | } ) -------------------------------------------------------------------------------- /src/lib/pick.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { pick } from './pick' 3 | 4 | const obj = { 5 | foo: 'foo', 6 | bar: 42, 7 | baz: true, 8 | } 9 | 10 | test( 'pick', () => { 11 | expect( pick( obj, [] ) ).toMatchObject( {} ) 12 | expect( pick( obj, [ 'foo' ] ) ).toMatchObject( { foo: 'foo' } ) 13 | expect( pick( obj, [ 'foo', 'bar' ] ) ).toMatchObject( { foo: 'foo', bar: 42 } ) 14 | } ) -------------------------------------------------------------------------------- /src/SPR.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { z } from 'zod' 3 | import { zu } from '.' 4 | 5 | test( 'README Example', () => { 6 | const schema = z.object( { foo: z.string() } ) 7 | const result = zu.SPR( schema.safeParse( { foo: 42 } ) ) 8 | const fooDataOrErrors = result.data?.foo ?? result.error?.format().foo?._errors 9 | 10 | expect( { fooDataOrErrors } ) 11 | .toMatchObject( { fooDataOrErrors: [ 'Expected string, received number' ] } ) 12 | } ) -------------------------------------------------------------------------------- /src/lib/mapValues.ts: -------------------------------------------------------------------------------- 1 | type ObjectIterator = 2 | ( value: Obj[ keyof Obj ], key: string, collection: Obj ) => Result 3 | 4 | export const mapValues = , Result> 5 | ( fn: ObjectIterator ) => ( obj?: Obj ) => { 6 | if ( !obj ) return {} 7 | const map = Object.keys( obj ).reduce( ( map, key ) => { 8 | map.set( key, fn( obj[ key ], key, obj ) ) 9 | return map 10 | }, new Map ) 11 | return Object.fromEntries( map ) 12 | } -------------------------------------------------------------------------------- /src/stringToJSON.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import type { json } from './json' 3 | 4 | const stringToJSONSchema = z.string() 5 | .transform( ( str, ctx ): z.infer> => { 6 | try { 7 | return JSON.parse( str ) 8 | } catch ( e ) { 9 | ctx.addIssue( { code: 'custom', message: 'Invalid JSON' } ) 10 | return z.NEVER 11 | } 12 | } ) 13 | 14 | /** 15 | zu.stringToJSON() is a schema that validates JSON encoded as a string, then returns the parsed value 16 | 17 | @example 18 | import { zu } from 'zod_utilz' 19 | const schema = zu.stringToJSON() 20 | schema.parse( 'true' ) // true 21 | schema.parse( 'null' ) // null 22 | schema.parse( '["one", "two", "three"]' ) // ['one', 'two', 'three'] 23 | schema.parse( 'not a JSON string' ) // throws 24 | */ 25 | export const stringToJSON = () => stringToJSONSchema -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "ESNext", 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "target": "ESNext", 10 | "module": "ESNext", 11 | "moduleDetection": "force", 12 | "jsx": "react-jsx", 13 | "allowJs": true, 14 | "moduleResolution": "bundler", 15 | "allowImportingTsExtensions": true, 16 | "noEmit": true, 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "composite": true, 22 | "downlevelIteration": true, 23 | "allowSyntheticDefaultImports": true, 24 | "types": [ 25 | "bun-types" 26 | ], 27 | }, 28 | "include": [ 29 | "./src/**/*", 30 | "package.json" 31 | ] 32 | } -------------------------------------------------------------------------------- /scripts/publish.ts: -------------------------------------------------------------------------------- 1 | import { $ } from 'bun' 2 | import * as Path from 'node:path' 3 | 4 | // https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 5 | // https://dev.to/astagi/publish-to-npm-using-github-actions-23fn 6 | 7 | const logError = ( ctx?: object ) => ( { message }: Error ) => { 8 | const data = { message, ...ctx } 9 | console.error( 'Error:', JSON.stringify( data, null, 2 ) ) 10 | } 11 | 12 | const root = Path.join( import.meta.dir, '..' ) 13 | const dist = Path.join( root, 'dist' ) 14 | 15 | await Promise.resolve() 16 | .then( () => console.log( 'Publishing...' ) ) 17 | 18 | .then( async () => { 19 | const section = 'npm publish' 20 | await $`cd ${ dist } && npm publish --access public` 21 | .then( () => console.log( 'npm publish: ran' ) ) 22 | .catch( logError( { section } ) ) 23 | } ) 24 | 25 | .then( () => console.log( 'Publish: done' ) ) 26 | .catch( logError( { path: import.meta.path } ) ) -------------------------------------------------------------------------------- /src/SPR.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | /** 4 | * SPR stands for SafeParseResult 5 | * 6 | * This enables [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) or [nullish coalescing](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing) for `z.SafeParseReturnType`. 7 | * 8 | * ### Usage: 9 | * ``` 10 | * import { zu } from 'zod_utilz' 11 | * const schema = z.object( { foo: z.string() } ) 12 | * const result = zu.SPR( schema.safeParse( { foo: 42 } ) ) 13 | * const fooDataOrErrors = result.data?.foo ?? result.error?.format().foo?._errors 14 | * ``` 15 | */ 16 | export function SPR ( result: z.SafeParseReturnType ): { 17 | success: typeof result[ 'success' ] 18 | data: z.SafeParseSuccess[ 'data' ] | undefined 19 | error: z.SafeParseError[ 'error' ] | undefined 20 | } { 21 | return result.success 22 | ? { ...result, error: undefined } 23 | : { ...result, data: undefined } 24 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zod_utilz", 3 | "version": "0.8.4", 4 | "author": "JacobWeisenburger", 5 | "description": "Framework agnostic utilities for Zod", 6 | "license": "MIT", 7 | "npm": "https://www.npmjs.com/package/zod_utilz", 8 | "repository": "https://github.com/JacobWeisenburger/zod_utilz", 9 | "homepage": "https://github.com/JacobWeisenburger/zod_utilz", 10 | "module": "index.js", 11 | "main": "index.js", 12 | "type": "module", 13 | "keywords": [ 14 | "typescript" 15 | ], 16 | "scripts": { 17 | "test": "clear && bun test --watch", 18 | "test.cmd": "cmd.exe /c start cmd /k bun run test", 19 | "build": "clear && bun --watch run scripts/build.ts", 20 | "publish": "clear && bun run scripts/publish.ts" 21 | }, 22 | "peerDependencies": { 23 | "typescript": "^5.0.0", 24 | "zod": "^3.22.4" 25 | }, 26 | "devDependencies": { 27 | "@types/bun": "latest", 28 | "bun-plugin-dts": "^0.2.1", 29 | "dts-bundle-generator": "^9.3.1", 30 | "@type-challenges/utils": "^0.1.1" 31 | } 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jacob Weisenburger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/json.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | const literalSchema = z.union( [ 4 | z.string(), 5 | z.number(), 6 | z.boolean(), 7 | z.null() 8 | ] ) 9 | 10 | type Literal = z.infer 11 | 12 | type Json = Literal | { [ key: string ]: Json } | Json[] 13 | 14 | const jsonSchema: z.ZodType = z.lazy( () => 15 | z.union( [ 16 | literalSchema, 17 | z.array( jsonSchema ), 18 | z.record( jsonSchema ) 19 | ] ) 20 | ) 21 | 22 | /** 23 | zu.json() is a schema that validates that a JavaScript object is JSON-compatible. This includes `string`, `number`, `boolean`, and `null`, plus `Array`s and `Object`s containing JSON-compatible types as values. 24 | Note: `JSON.stringify()` enforces non-circularity, but this can't be easily checked without actually stringifying the results, which can be slow. 25 | @example 26 | import { zu } from 'zod_utilz' 27 | const schema = zu.json() 28 | schema.parse( false ) // false 29 | schema.parse( 8675309 ) // 8675309 30 | schema.parse( { a: 'deeply', nested: [ 'JSON', 'object' ] } ) 31 | // { a: 'deeply', nested: [ 'JSON', 'object' ] } 32 | */ 33 | export const json = () => jsonSchema -------------------------------------------------------------------------------- /src/json.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { zu } from '.' 3 | 4 | test( 'json', () => { 5 | const schema = zu.json() 6 | 7 | /* allowed primitives */ 8 | expect( schema.parse( 'foo' ) ).toBe( 'foo' ) 9 | expect( schema.parse( 42 ) ).toBe( 42 ) 10 | expect( schema.parse( true ) ).toBe( true ) 11 | expect( schema.parse( false ) ).toBe( false ) 12 | expect( schema.parse( null ) ).toBe( null ) 13 | 14 | /* disallowed primitives */ 15 | expect( () => schema.parse( 42n ) ).toThrow() 16 | expect( () => schema.parse( Symbol( 'symbol' ) ) ).toThrow() 17 | expect( () => schema.parse( undefined ) ).toThrow() 18 | expect( () => schema.parse( new Date() ) ).toThrow() 19 | 20 | /* objects */ 21 | const nested = { one: [ 'two', { three: 4 } ] } 22 | expect( schema.parse( nested ) ).toMatchObject( nested ) 23 | } ) 24 | 25 | test( 'README Example', () => { 26 | const schema = zu.json() 27 | expect( schema.parse( false ) ).toBe( false ) 28 | expect( schema.parse( 8675309 ) ).toBe( 8675309 ) 29 | expect( schema.parse( { a: 'deeply', nested: [ 'JSON', 'object' ] } ) ) 30 | .toMatchObject( { a: 'deeply', nested: [ 'JSON', 'object' ] } ) 31 | } ) -------------------------------------------------------------------------------- /src/useTypedParsers.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | type ParseMethods = 'parse' | 'parseAsync' | 'safeParse' | 'safeParseAsync' 4 | 5 | /** 6 | * Enables compile time type checking for zod parsers. 7 | * 8 | * ### Usage: 9 | * ``` 10 | * import { zu } from 'zod_utilz' 11 | * const schemaWithTypedParsers = zu.useTypedParsers( z.literal( 'foo' ) ) 12 | * 13 | * schemaWithTypedParsers.parse( 'foo' ) 14 | * // no ts errors 15 | * 16 | * schemaWithTypedParsers.parse( 'bar' ) 17 | * // ^^^^^ 18 | * // Argument of type '"bar"' is not assignable to parameter of type '"foo"' 19 | * ``` 20 | */ 21 | export const useTypedParsers = 22 | ( schema: Schema ) => schema as any as TypedParsersSchema 23 | 24 | type ParametersExceptFirst = 25 | Func extends ( arg0: any, ...rest: infer R ) => any ? R : never 26 | 27 | type Params = [ 28 | data: z.input, 29 | ...rest: ParametersExceptFirst 30 | ] 31 | 32 | type TypedParsersSchema = Omit & { 33 | [ Method in ParseMethods ]: ( ...args: Params ) => ReturnType 34 | } -------------------------------------------------------------------------------- /src/stringToJSON.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { z } from 'zod' 3 | import { zu } from '.' 4 | 5 | test( 'stringToJSON', () => { 6 | const schema = zu.stringToJSON() 7 | 8 | /* allowed primitives */ 9 | expect( schema.parse( '"foo"' ) ).toBe( 'foo' ) 10 | expect( schema.parse( '42' ) ).toBe( 42 ) 11 | expect( schema.parse( 'true' ) ).toBe( true ) 12 | expect( schema.parse( 'false' ) ).toBe( false ) 13 | expect( schema.parse( 'null' ) ).toBe( null ) 14 | 15 | /* disallowed primitives */ 16 | expect( () => schema.parse( '42n' ) ).toThrow() 17 | expect( () => schema.parse( 'undefined' ) ).toThrow() 18 | 19 | /* valid objects */ 20 | const nested = { one: [ 'two', { three: 4 } ] } 21 | expect( schema.parse( JSON.stringify( nested ) ) ).toMatchObject( nested ) 22 | 23 | /* invalid JSON */ 24 | expect( () => schema.parse( '{ keys: "must be quoted" }' ) ).toThrow() 25 | expect( () => schema.parse( '{ "objects": "must be closed"' ) ).toThrow() 26 | expect( () => schema.parse( '"arrays", "must", "be", "opened" ]' ) ).toThrow() 27 | expect( () => schema.parse( 'is not JSON' ) ).toThrow() 28 | 29 | /* piping */ 30 | const jsonNumberSchema = zu.stringToJSON().pipe( z.number() ) 31 | expect( jsonNumberSchema.parse( '500' ) ).toBe( 500 ) 32 | expect( () => jsonNumberSchema.parse( '"JSON, but not a number"' ) ).toThrow() 33 | } ) 34 | 35 | test( 'stringToJSON', () => { 36 | const schema = zu.stringToJSON() 37 | 38 | expect( schema.parse( 'true' ) ).toBe( true ) 39 | expect( schema.parse( 'null' ) ).toBe( null ) 40 | expect( schema.parse( '["one", "two", "three"]' ) ) 41 | .toMatchObject( [ 'one', 'two', 'three' ] ) 42 | expect( () => schema.parse( 'not a JSON string' ) ).toThrow() 43 | } ) -------------------------------------------------------------------------------- /src/partialSafeParse.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { zu } from '.' 3 | import { mapValues } from './lib/mapValues' 4 | import { pick } from './lib/pick' 5 | import { omit } from './lib/omit' 6 | 7 | /** 8 | partialSafeParse allows you to get the valid fields even if there was an error in another field 9 | 10 | @example 11 | import { zu } from 'zod_utilz' 12 | const userSchema = z.object( { name: z.string(), age: z.number() } ) 13 | const result = zu.partialSafeParse( userSchema, { name: null, age: 42 } ) 14 | // { 15 | // successType: 'partial', 16 | // validData: { age: 42 }, 17 | // invalidData: { name: null }, 18 | // } 19 | result.error?.flatten().fieldErrors 20 | // { 21 | // name: [ 'Expected string, received null' ], 22 | // } 23 | */ 24 | export function partialSafeParse ( 25 | schema: Schema, input: unknown 26 | ): ReturnType & { 27 | successType: 'full' | 'partial' | 'none', 28 | validData: Partial>, 29 | invalidData: Partial>, 30 | } { 31 | const result = zu.SPR( schema.safeParse( input ) ) 32 | if ( result.success ) return { 33 | ...result, 34 | successType: 'full', 35 | validData: result.data as Partial>, 36 | invalidData: {}, 37 | } as const 38 | 39 | const { fieldErrors, formErrors } = result.error?.flatten() ?? {} 40 | if ( formErrors?.length ) return { 41 | ...result, 42 | successType: 'none', 43 | validData: {}, 44 | invalidData: {}, 45 | } 46 | 47 | const inputObj = input as z.infer 48 | const keysWithInvalidData = Object.keys( fieldErrors ?? {} ) 49 | const validInput = omit( inputObj, keysWithInvalidData ) 50 | const invalidData = pick( inputObj, keysWithInvalidData ) as Partial> 51 | 52 | const validData = schema 53 | .omit( mapValues( () => true )( fieldErrors ) ) 54 | .parse( validInput ) as Partial> 55 | 56 | return { 57 | ...result, 58 | successType: 'partial', 59 | validData, 60 | invalidData, 61 | } 62 | } -------------------------------------------------------------------------------- /src/useTypedParsers.test.ts: -------------------------------------------------------------------------------- 1 | import { Equal, Expect, NotEqual } from '@type-challenges/utils' 2 | import { expect, test } from 'bun:test' 3 | import { z } from 'zod' 4 | import { zu } from '.' 5 | 6 | test( 'useTypedParsers type tests', () => { 7 | const schemaWithTypedParse = zu.useTypedParsers( z.object( { 8 | foo: z.literal( 'foo' ), 9 | } ) ) 10 | 11 | type parseTest = Expect[ 0 ], 13 | z.infer 14 | >> 15 | type safeParseTest = Expect[ 0 ], 17 | z.infer 18 | >> 19 | type parseAsyncTest = Expect[ 0 ], 21 | z.infer 22 | >> 23 | type safeParseAsyncTest = Expect[ 0 ], 25 | z.infer 26 | >> 27 | 28 | type parseTestError = Expect[ 0 ], 30 | 'hello' 31 | >> 32 | type safeParseTestError = Expect[ 0 ], 34 | 'hello' 35 | >> 36 | type parseAsyncTestError = Expect[ 0 ], 38 | 'hello' 39 | >> 40 | type safeParseAsyncTestError = Expect[ 0 ], 42 | 'hello' 43 | >> 44 | } ) 45 | 46 | test( 'README Example', () => { 47 | const schemaWithTypedParsers = zu.useTypedParsers( z.literal( 'foo' ) ) 48 | 49 | // no ts errors 50 | expect( schemaWithTypedParsers.parse( 'foo' ) ).toBe( 'foo' ) 51 | 52 | expect( () => 53 | // @ts-expect-error 54 | schemaWithTypedParsers.parse( 'bar' ) 55 | // ^^^^^ 56 | // Argument of type '"bar"' is not assignable to parameter of type '"foo"' 57 | ).toThrow() 58 | } ) 59 | 60 | test( 'https://github.com/JacobWeisenburger/zod_utilz/issues/13', () => { 61 | const DEFAULT_CONCURRENCY = 1 62 | const schema = z.number().int().min( 1 ).default( DEFAULT_CONCURRENCY ) 63 | const typedSchema = zu.useTypedParsers( schema ) 64 | type Input = z.input 65 | // type Input = number | undefined 66 | type Output = z.output 67 | // type Output = number 68 | typedSchema.parse( undefined ) 69 | } ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /src/makeErrorMap.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const ExtraErrorCode = z.util.arrayToEnum( [ 4 | 'required', 5 | ] ) 6 | 7 | type RequiredIssue = z.ZodIssueBase & { 8 | code: typeof ExtraErrorCode.required 9 | expected: z.ZodParsedType 10 | received: 'undefined' 11 | } 12 | 13 | type ExtraErrorCode = keyof typeof ExtraErrorCode 14 | 15 | export type ErrorCode = ExtraErrorCode | z.ZodIssueCode 16 | 17 | type Issue = 18 | Code extends RequiredIssue[ 'code' ] 19 | ? RequiredIssue 20 | : Code extends z.ZodIssueCode 21 | ? z.ZodIssueOptionalMessage & { code: Code } 22 | : never 23 | 24 | export type ErrorMapMessageBuilderContext = 25 | z.ErrorMapCtx & Issue 26 | 27 | export type ErrorMapMessage = string 28 | 29 | export type ErrorMapMessageBuilder = 30 | ( context: ErrorMapMessageBuilderContext ) => ErrorMapMessage 31 | 32 | export type ErrorMapConfig = { 33 | [ Code in ErrorCode ]?: ErrorMapMessage | ErrorMapMessageBuilder 34 | } 35 | // type ErrorMapConfigRecord = Record 36 | // export type ErrorMapConfig = Partial 37 | 38 | /** 39 | * Simplifies the process of making a `ZodErrorMap` 40 | * 41 | * ### Usage: 42 | * ``` 43 | * import { zu } from 'zod_utilz' 44 | * const errorMap = zu.makeErrorMap( { 45 | * required: 'Custom required message', 46 | * invalid_type: ( { data } ) => `${ data } is an invalid type`, 47 | * too_big: ( { maximum } ) => `Maximum length is ${ maximum }`, 48 | * invalid_enum_value: ( { data, options } ) => 49 | * `${ data } is not a valid enum value. Valid options: ${ options?.join( ' | ' ) } `, 50 | * } ) 51 | * 52 | * const stringSchema = z.string( { errorMap } ).max( 32 ) 53 | * 54 | * zu.SPR( stringSchema.safeParse( undefined ) ).error?.issues[ 0 ].message 55 | * // Custom required message 56 | * 57 | * zu.SPR( stringSchema.safeParse( 42 ) ).error?.issues[ 0 ].message 58 | * // 42 is an invalid type 59 | * 60 | * zu.SPR( stringSchema.safeParse( 61 | * 'this string is over the maximum length' 62 | * ) ).error?.issues[ 0 ].message 63 | * // Maximum length is 32 64 | * 65 | * const enumSchema = z.enum( [ 'foo', 'bar' ], { errorMap } ) 66 | * 67 | * zu.SPR( enumSchema.safeParse( 'baz' ) ).error?.issues[ 0 ].message 68 | * // baz is not a valid enum value. Valid options: foo | bar 69 | * ``` 70 | */ 71 | export function makeErrorMap ( config: ErrorMapConfig ): z.ZodErrorMap { 72 | return ( issue, ctx ) => { 73 | const errorCode: ErrorCode = 74 | issue.code === 'invalid_type' && ctx.data === undefined 75 | ? 'required' : issue.code 76 | 77 | const messageOrBuilder = config[ errorCode ] 78 | const context = { ...ctx, ...issue, code: errorCode } 79 | 80 | const message = typeof messageOrBuilder === 'function' 81 | /* @ts-ignore */ 82 | // TODO figure out how to deal with: 83 | // Expression produces a union type that is too complex to represent. 84 | ? messageOrBuilder( context ) 85 | : messageOrBuilder 86 | 87 | return message ? { message } : { message: ctx.defaultError } 88 | } 89 | } -------------------------------------------------------------------------------- /src/useFormLike.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | function safeParseJSON ( string: string ): any { 4 | try { return JSON.parse( string ) } 5 | catch { return string } 6 | } 7 | 8 | const formLikeToRecord = ( keys?: string[] ) => 9 | ( formLike: FormData | URLSearchParams ): Record => { 10 | return Array.from( keys ?? formLike.keys() ).reduce( ( record, key ) => { 11 | const values = formLike.getAll( key ) 12 | .map( x => x instanceof File ? x : safeParseJSON( x ) ) 13 | record[ key ] = values.length > 1 ? values : values[ 0 ] 14 | return record 15 | }, {} as Record ) 16 | } 17 | 18 | const useFormLike = ( type: typeof FormData | typeof URLSearchParams ) => < 19 | Schema extends z.ZodObject 20 | > ( schema: Schema ) => { 21 | const { unknownKeys } = schema._def 22 | const keys = unknownKeys == 'strip' ? Object.keys( schema.shape ) : undefined 23 | return z.instanceof( type ) 24 | .transform( formLikeToRecord( keys ) ) 25 | .pipe( schema ) 26 | } 27 | 28 | /** 29 | * A way to parse URLSearchParams 30 | * 31 | * ### Usage: 32 | * ``` 33 | * import { zu } from 'zod_utilz' 34 | * const schema = zu.useURLSearchParams( 35 | * z.object( { 36 | * string: z.string(), 37 | * number: z.number(), 38 | * boolean: z.boolean(), 39 | * } ) 40 | * ) 41 | * 42 | * zu.SPR( schema.safeParse( 43 | * new URLSearchParams( { 44 | * string: 'foo', 45 | * number: '42', 46 | * boolean: 'false', 47 | * } ) 48 | * ) ).data 49 | * // { string: 'foo', number: 42, boolean: false } 50 | * 51 | * zu.SPR( schema.safeParse( 52 | * new URLSearchParams( { 53 | * string: '42', 54 | * number: 'false', 55 | * boolean: 'foo', 56 | * } ) 57 | * ) ).error?.flatten().fieldErrors 58 | * // { 59 | * // string: [ 'Expected string, received number' ], 60 | * // number: [ 'Expected number, received boolean' ], 61 | * // boolean: [ 'Expected boolean, received string' ], 62 | * // } 63 | * ``` 64 | */ 65 | export const useURLSearchParams = > 66 | ( schema: Schema ) => useFormLike( URLSearchParams )( schema ) 67 | 68 | /** 69 | A way to parse FormData 70 | 71 | ``` 72 | const schema = zu.useFormData( 73 | z.object( { 74 | string: z.string(), 75 | number: z.number(), 76 | boolean: z.boolean(), 77 | file: z.instanceof( File ), 78 | } ) 79 | ) 80 | ``` 81 | 82 | @example 83 | import { zu } from 'zod_utilz' 84 | const file = new File( [], 'filename.ext' ) 85 | const formData = new FormData() 86 | formData.append( 'string', 'foo' ) 87 | formData.append( 'number', '42' ) 88 | formData.append( 'boolean', 'false' ) 89 | formData.append( 'file', file ) 90 | 91 | zu.SPR( schema.safeParse( formData ) ).data, 92 | // { string: 'foo', number: 42, boolean: false, file } 93 | 94 | @example 95 | import { zu } from 'zod_utilz' 96 | const formData = new FormData() 97 | formData.append( 'string', '42' ) 98 | formData.append( 'number', 'false' ) 99 | formData.append( 'boolean', 'foo' ) 100 | formData.append( 'file', 'filename.ext' ) 101 | 102 | zu.SPR( schema.safeParse( formData ) ).error?.flatten().fieldErrors, 103 | // { 104 | // string: [ 'Expected string, received number' ], 105 | // number: [ 'Expected number, received boolean' ], 106 | // boolean: [ 'Expected boolean, received string' ], 107 | // file: [ 'Input not instance of File' ], 108 | // } 109 | */ 110 | export const useFormData = > 111 | ( schema: Schema ) => useFormLike( FormData )( schema ) -------------------------------------------------------------------------------- /src/partialSafeParse.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { z } from 'zod' 3 | import { zu } from '.' 4 | 5 | const userSchema = z.object( { name: z.string(), age: z.number() } ) 6 | 7 | test( `successType: 'full'`, () => { 8 | const result = zu.partialSafeParse( userSchema, { name: 'foo', age: 42 } ) 9 | expect( result ).toMatchObject( { 10 | successType: 'full', 11 | data: { name: 'foo', age: 42 }, 12 | validData: { name: 'foo', age: 42 }, 13 | invalidData: {}, 14 | } ) 15 | 16 | // @ts-ignore 17 | expect( result.error ).toBe( undefined ) 18 | } ) 19 | 20 | test( `successType: 'partial': { name: null, age: 42 }`, () => { 21 | const result = zu.partialSafeParse( userSchema, { name: null, age: 42 } ) 22 | expect( result ).toMatchObject( { 23 | successType: 'partial', 24 | validData: { age: 42 }, 25 | invalidData: { name: null }, 26 | } ) 27 | expect( result.error?.flatten().fieldErrors ?? {} ).toMatchObject( { 28 | name: [ 'Expected string, received null' ] 29 | } ) 30 | } ) 31 | 32 | test( `successType: 'partial': { name: null }`, () => { 33 | const result = zu.partialSafeParse( userSchema, { name: null } ) 34 | expect( result ).toMatchObject( { 35 | successType: 'partial', 36 | validData: {}, 37 | invalidData: { name: null }, 38 | } ) 39 | expect( result.error?.flatten().fieldErrors ?? {} ).toMatchObject( { 40 | name: [ 'Expected string, received null' ], 41 | age: [ 'Required' ], 42 | } ) 43 | } ) 44 | 45 | test( `successType: 'partial': {}`, () => { 46 | const result = zu.partialSafeParse( userSchema, {} ) 47 | expect( result ).toMatchObject( { 48 | successType: 'partial', 49 | validData: {}, 50 | invalidData: {}, 51 | } ) 52 | expect( result.error?.flatten().fieldErrors ?? {} ).toMatchObject( { 53 | name: [ 'Required' ], 54 | age: [ 'Required' ], 55 | } ) 56 | } ) 57 | 58 | test( `successType: 'none'`, () => { 59 | const result = zu.partialSafeParse( userSchema, null ) 60 | expect( result ).toMatchObject( { 61 | successType: 'none', 62 | validData: {}, 63 | invalidData: {}, 64 | } ) 65 | expect( result.error?.flatten().formErrors ?? [] ).toMatchObject( [ 66 | 'Expected object, received null' 67 | ] ) 68 | } ) 69 | 70 | test( `Readme Example`, () => { 71 | const userSchema = z.object( { name: z.string(), age: z.number() } ) 72 | const result = zu.partialSafeParse( userSchema, { name: null, age: 42 } ) 73 | expect( result ).toMatchObject( { 74 | successType: 'partial', 75 | validData: { age: 42 }, 76 | invalidData: { name: null }, 77 | } ) 78 | expect( result.error?.flatten().fieldErrors ?? {} ).toMatchObject( { 79 | name: [ 'Expected string, received null' ] 80 | } ) 81 | } ) 82 | 83 | // test( `with useURLSearchParams`, () => { 84 | // const params = new URLSearchParams( 'foo=foo&bar=42' ) 85 | // const schema = zu.useURLSearchParams( 86 | // z.object( { foo: z.string(), bar: z.string() } ) 87 | // ) 88 | // // const schema: z.ZodPipeline, Record, FormData | URLSearchParams>, z.ZodObject<...>> 89 | 90 | 91 | // //@ts-ignore 92 | // const result = zu.partialSafeParse( schema, params ) 93 | // console.log( result ) 94 | // // expect( 95 | // // result, 96 | // // { 97 | // // successType: 'partial', 98 | // // validData: { age: 42 }, 99 | // // invalidData: { name: null }, 100 | // // } 101 | // // ) 102 | // // expect( 103 | // // result.error?.flatten().fieldErrors ?? {}, 104 | // // { 105 | // // name: [ 'Expected string, received null' ], 106 | // // } 107 | // // ) 108 | // } ) -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import type { BuildConfig, BunPlugin } from 'bun' 2 | import { EntryPointConfig, generateDtsBundle } from 'dts-bundle-generator' 3 | import { copyFile, rm } from 'node:fs/promises' 4 | import * as Path from 'node:path' 5 | import packageJSON from '../package.json' 6 | 7 | // https://bun.sh/docs/bundler 8 | // https://github.com/wobsoriano/bun-plugin-dts 9 | // https://github.com/timocov/dts-bundle-generator 10 | 11 | const logError = ( ctx?: object ) => ( { message }: Error ) => { 12 | const data = { message, ...ctx } 13 | console.error( 'Error:', JSON.stringify( data, null, 2 ) ) 14 | } 15 | 16 | const root = Path.join( import.meta.dir, '..' ) 17 | const dist = Path.join( root, 'dist' ) 18 | 19 | function customDtsPlugin (): BunPlugin { 20 | const output: EntryPointConfig[ 'output' ] = { noBanner: true } 21 | return { 22 | name: 'customDtsPlugin', 23 | target: 'node', 24 | setup ( build ) { 25 | const outDir = build.config.outdir || './dist' 26 | 27 | build.config.entrypoints 28 | .map( filePath => ( { filePath, output } ) ) 29 | .forEach( config => { 30 | const [ output ] = generateDtsBundle( [ config ] ) 31 | const dtsFileName = config.filePath 32 | .replace( /^.*\//, '' ) 33 | .replace( /\.[jtm]s$/, '.d.ts' ) 34 | const outFile = Path.join( outDir, dtsFileName ) 35 | return Bun.write( outFile, output ) 36 | } ) 37 | }, 38 | } 39 | } 40 | 41 | await Promise.resolve() 42 | .then( () => console.log( 'Building...' ) ) 43 | .then( () => 44 | console.log( { 45 | root, 46 | dist, 47 | } ) 48 | ) 49 | 50 | .then( async () => { 51 | await rm( dist, { recursive: true } ) 52 | .then( () => console.log( 'dist: deleted' ) ) 53 | .catch( logError( { dist } ) ) 54 | } ) 55 | 56 | .then( async () => { 57 | const section = 'Bun.build' 58 | const config: BuildConfig = { 59 | entrypoints: [ './src/index.ts' ], 60 | format: 'esm', 61 | sourcemap: 'inline', 62 | target: 'node', 63 | plugins: [ 64 | customDtsPlugin() 65 | ], 66 | } 67 | await Bun.build( config ) 68 | .then( buildResult => Promise.all( 69 | buildResult.outputs.map( res => Bun.write( Path.join( dist, res.path ), res ) ) 70 | ) ) 71 | .then( () => console.log( 'Bun.build: ran' ) ) 72 | .catch( logError( { section, config } ) ) 73 | } ) 74 | 75 | .then( async () => { 76 | const section = 'Reduced package.json: written' 77 | const distPath = Path.join( dist, 'package.json' ) 78 | const { scripts, devDependencies, ...reduced } = packageJSON 79 | 80 | await Bun.write( distPath, JSON.stringify( reduced, null, 4 ) ) 81 | .then( () => console.log( section ) ) 82 | .catch( logError( { section, distPath, reduced } ) ) 83 | } ) 84 | 85 | .then( async () => { 86 | const section = 'Reduced README: written' 87 | const srcPath = Path.join( root, 'README.md' ) 88 | const distPath = Path.join( dist, 'README.md' ) 89 | const contents = await Bun.file( srcPath ).text() 90 | const newContents = contents 91 | .split( '' )[ 0 ] 92 | .trim() 93 | 94 | await Bun.write( distPath, newContents ) 95 | .then( () => console.log( section ) ) 96 | .catch( logError( { section, distPath, contents } ) ) 97 | } ) 98 | 99 | .then( async () => { 100 | const section = 'LICENSE' 101 | const srcPath = Path.join( root, 'LICENSE' ) 102 | const distPath = Path.join( dist, 'LICENSE' ) 103 | await copyFile( srcPath, distPath ) 104 | .then( () => console.log( 'LICENSE: copied' ) ) 105 | .catch( logError( { section, srcPath, distPath } ) ) 106 | } ) 107 | 108 | .then( () => console.log( 'Build: done' ) ) 109 | .catch( logError( { path: import.meta.path } ) ) -------------------------------------------------------------------------------- /src/coerce.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | // https://developer.mozilla.org/en-US/docs/Glossary/Primitive 4 | // string 5 | // number 6 | // bigint 7 | // boolean 8 | // undefined 9 | // symbol 10 | // null 11 | 12 | type AllowedZodTypes = 13 | | z.ZodAny 14 | | z.ZodString 15 | | z.ZodNumber 16 | | z.ZodBoolean 17 | | z.ZodBigInt 18 | // | z.ZodDate // TODO 19 | | z.ZodArray 20 | // | z.ZodObject // TODO 21 | 22 | /** 23 | * Treats coercion errors like normal zod errors. Prevents throwing errors when using `safeParse`. 24 | * 25 | * @example 26 | * import { zu } from 'zod_utilz' 27 | * const bigintSchema = zu.coerce( z.bigint() ) 28 | * bigintSchema.parse( '42' ) // 42n 29 | * bigintSchema.parse( '42n' ) // 42n 30 | * zu.SPR( bigintSchema.safeParse( 'foo' ) ).error?.issues[ 0 ].message 31 | * // 'Expected bigint, received string' 32 | * 33 | * @example 34 | * import { zu } from 'zod_utilz' 35 | * const booleanSchema = zu.coerce( z.boolean() ) 36 | * 37 | * // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean 38 | * // only exception to normal boolean coercion rules 39 | * booleanSchema.parse( 'false' ) // false 40 | * 41 | * // https://developer.mozilla.org/en-US/docs/Glossary/Falsy 42 | * // falsy => false 43 | * booleanSchema.parse( false ) // false 44 | * booleanSchema.parse( 0 ) // false 45 | * booleanSchema.parse( -0 ) // false 46 | * booleanSchema.parse( 0n ) // false 47 | * booleanSchema.parse( '' ) // false 48 | * booleanSchema.parse( null ) // false 49 | * booleanSchema.parse( undefined ) // false 50 | * booleanSchema.parse( NaN ) // false 51 | * 52 | * // truthy => true 53 | * booleanSchema.parse( 'foo' ) // true 54 | * booleanSchema.parse( 42 ) // true 55 | * booleanSchema.parse( [] ) // true 56 | * booleanSchema.parse( {} ) // true 57 | * 58 | * @example 59 | * import { zu } from 'zod_utilz' 60 | * const numberArraySchema = zu.coerce( z.number().array() ) 61 | * 62 | * // if the value is not an array, it is coerced to an array with one coerced item 63 | * numberArraySchema.parse( 42 ) // [ 42 ] 64 | * numberArraySchema.parse( '42' ) // [ 42 ] 65 | * 66 | * // if the value is an array, it coerces each item in the array 67 | * numberArraySchema.parse( [] ) // [] 68 | * numberArraySchema.parse( [ '42', 42 ] ) // [ 42, 42 ] 69 | * 70 | * zu.SPR( numberArraySchema.safeParse( 'foo' ) ).error?.issues[ 0 ].message 71 | * // 'Expected number, received nan' 72 | */ 73 | export function coerce ( schema: Schema ) { 74 | return z.any() 75 | .transform>( getTransformer( schema ) ) 76 | .pipe( schema ) as z.ZodPipeline, any>, Schema> 77 | } 78 | 79 | function getTransformer ( schema: Schema ) { 80 | if ( schema._def.typeName === 'ZodAny' ) return ( value: any ) => value 81 | if ( schema._def.typeName === 'ZodString' ) return toString 82 | if ( schema._def.typeName === 'ZodNumber' ) return toNumber 83 | if ( schema._def.typeName === 'ZodBoolean' ) return toBoolean 84 | 85 | if ( schema._def.typeName === 'ZodBigInt' ) return toBigInt 86 | if ( schema._def.typeName === 'ZodArray' ) return toArray( schema as z.ZodArray ) 87 | 88 | throw new Error( `${ schema!.constructor.name } is not supported by zu.coerce` ) 89 | } 90 | 91 | function toString ( value: any ): string { 92 | if ( typeof value === 'string' ) return value 93 | 94 | try { 95 | const newValue = JSON.stringify( value ) 96 | if ( newValue == undefined ) throw 'JSON.stringify returned undefined' 97 | return newValue 98 | } catch ( error ) { } 99 | try { 100 | return String( value ) 101 | } catch ( error ) { } 102 | 103 | return value 104 | } 105 | 106 | function toNumber ( value: any ): number { 107 | if ( typeof value === 'number' ) return value 108 | if ( typeof value === 'object' ) return NaN 109 | if ( value == null ) return NaN 110 | 111 | try { 112 | return Number( value ) 113 | } catch ( error ) { } 114 | 115 | return NaN 116 | } 117 | 118 | function toBigInt ( value: any ): bigint { 119 | if ( typeof value === 'bigint' ) return value 120 | 121 | if ( typeof value === 'string' && value.endsWith( 'n' ) ) { 122 | try { 123 | const parsed = BigInt( value.slice( 0, -1 ) ) 124 | if ( value == `${ parsed.toString() }n` ) return parsed 125 | } catch { } 126 | } 127 | 128 | try { return BigInt( toNumber( value ) ) } 129 | catch ( error ) { } 130 | 131 | return value 132 | } 133 | 134 | function toBoolean ( value: any ): boolean { 135 | if ( typeof value === 'boolean' ) return value 136 | 137 | try { 138 | return Boolean( JSON.parse( value ) ) 139 | } catch ( error ) { } 140 | try { 141 | return Boolean( value ) 142 | } catch ( error ) { } 143 | 144 | return false 145 | } 146 | 147 | function toArray> ( schema: Schema ) { 148 | const itemTransformer = getTransformer( schema._def.type ) 149 | return ( value: z.input ): z.output[] => { 150 | if ( Array.isArray( value ) ) return value.map( itemTransformer ) 151 | return [ value ].map( itemTransformer ) 152 | } 153 | } -------------------------------------------------------------------------------- /src/makeErrorMap.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { z } from 'zod' 3 | import { zu } from '.' 4 | 5 | const config = { 6 | required: ctx => 'required', 7 | invalid_type: ctx => 'invalid_type', 8 | too_small: ctx => 'too_small', 9 | too_big: ctx => 'too_big', 10 | invalid_string: ctx => 'invalid_string', 11 | not_multiple_of: ctx => 'not_multiple_of', 12 | not_finite: ctx => 'not_finite', 13 | invalid_enum_value: ctx => 'invalid_enum_value', 14 | invalid_union_discriminator: ctx => 'invalid_union_discriminator', 15 | } satisfies Parameters[ 0 ] 16 | 17 | const errorMap = zu.makeErrorMap( config ) 18 | 19 | const errorCtxMock = { 20 | data: 'unknown', 21 | defaultError: 'Invalid', 22 | code: 'invalid_type', 23 | expected: 'unknown', 24 | received: 'unknown', 25 | path: [], 26 | } satisfies zu.ErrorMapMessageBuilderContext 27 | 28 | test( 'z.string( { errorMap } )', () => { 29 | const schema = z.string( { errorMap } ).min( 3 ).max( 5 ) 30 | 31 | expect( zu.SPR( schema.safeParse( 'foo' ) ).data ).toBe( 'foo' ) 32 | 33 | expect( zu.SPR( schema.safeParse( undefined ) ).error?.issues[ 0 ].message ) 34 | .toBe( 35 | config.required( { 36 | ...errorCtxMock, 37 | code: 'required', 38 | received: 'undefined', 39 | } ) 40 | ) 41 | 42 | expect( zu.SPR( schema.safeParse( 42 ) ).error?.issues[ 0 ].message ) 43 | .toBe( config.invalid_type( errorCtxMock ) ) 44 | 45 | expect( zu.SPR( schema.safeParse( [ 'foo' ] ) ).error?.issues[ 0 ].message ) 46 | .toBe( config.invalid_type( errorCtxMock ) ) 47 | 48 | expect( zu.SPR( schema.safeParse( 'ha' ) ).error?.issues[ 0 ].message ) 49 | .toBe( 50 | config.too_small( { 51 | ...errorCtxMock, 52 | code: 'too_small', 53 | type: 'string', 54 | inclusive: true, 55 | minimum: 3, 56 | } ) 57 | ) 58 | 59 | expect( zu.SPR( schema.safeParse( 'hello world' ) ).error?.issues[ 0 ].message ) 60 | .toBe( 61 | config.too_big( { 62 | ...errorCtxMock, 63 | code: 'too_big', 64 | type: 'string', 65 | inclusive: true, 66 | maximum: 5, 67 | } ) 68 | ) 69 | } ) 70 | 71 | test( 'z.number( { errorMap } )', () => { 72 | const schema = z.number( { errorMap } ).min( 3 ).max( 5 ) 73 | 74 | expect( zu.SPR( schema.safeParse( 3 ) ).data ).toBe( 3 ) 75 | 76 | expect( zu.SPR( schema.safeParse( undefined ) ).error?.issues[ 0 ].message ) 77 | .toBe( 78 | config.required( { 79 | ...errorCtxMock, 80 | code: 'required', 81 | received: 'undefined', 82 | } ) 83 | ) 84 | 85 | expect( zu.SPR( schema.safeParse( 'foo' ) ).error?.issues[ 0 ].message ) 86 | .toBe( config.invalid_type( errorCtxMock ) ) 87 | 88 | expect( zu.SPR( schema.safeParse( [ 42 ] ) ).error?.issues[ 0 ].message ) 89 | .toBe( config.invalid_type( errorCtxMock ) ) 90 | 91 | expect( zu.SPR( schema.safeParse( 2 ) ).error?.issues[ 0 ].message ) 92 | .toBe( 93 | config.too_small( { 94 | ...errorCtxMock, 95 | code: 'too_small', 96 | type: 'number', 97 | inclusive: true, 98 | minimum: 3, 99 | } ) 100 | ) 101 | 102 | expect( zu.SPR( schema.safeParse( 6 ) ).error?.issues[ 0 ].message ) 103 | .toBe( 104 | config.too_big( { 105 | ...errorCtxMock, 106 | code: 'too_big', 107 | type: 'number', 108 | inclusive: true, 109 | maximum: 5, 110 | } ) 111 | ) 112 | } ) 113 | 114 | test( `z.enum( [ 'foo', 'bar', 'baz' ], { errorMap } )`, () => { 115 | const schema = z.enum( [ 'foo', 'bar', 'baz' ], { errorMap } ) 116 | 117 | expect( zu.SPR( schema.safeParse( 'foo' ) ).data ).toBe( 'foo' ) 118 | 119 | expect( zu.SPR( schema.safeParse( undefined ) ).error?.issues[ 0 ].message ) 120 | .toBe( 121 | config.required( { 122 | ...errorCtxMock, 123 | code: 'required', 124 | received: 'undefined', 125 | } ) 126 | ) 127 | 128 | expect( zu.SPR( schema.safeParse( 42 ) ).error?.issues[ 0 ].message ) 129 | .toBe( config.invalid_type( errorCtxMock ) ) 130 | 131 | expect( zu.SPR( schema.safeParse( [ 'foo' ] ) ).error?.issues[ 0 ].message ) 132 | .toBe( config.invalid_type( errorCtxMock ) ) 133 | } ) 134 | 135 | test( 'z.date( { errorMap } )', () => { 136 | const schema = z.date( { errorMap } ) 137 | 138 | const now = new Date 139 | expect( zu.SPR( schema.safeParse( now ) ).data ).toMatchObject( now ) 140 | 141 | expect( zu.SPR( schema.safeParse( undefined ) ).error?.issues[ 0 ].message ) 142 | .toBe( 143 | config.required( { 144 | ...errorCtxMock, 145 | code: 'required', 146 | received: 'undefined', 147 | } ) 148 | ) 149 | 150 | expect( zu.SPR( schema.safeParse( '2023-01-13' ) ).error?.issues[ 0 ].message ) 151 | .toBe( config.invalid_type( errorCtxMock ) ) 152 | 153 | expect( zu.SPR( schema.safeParse( null ) ).error?.issues[ 0 ].message ) 154 | .toBe( config.invalid_type( errorCtxMock ) ) 155 | } ) 156 | 157 | test( 'README Example', () => { 158 | const config = { 159 | required: 'Custom required message', 160 | invalid_type: ( { data } ) => `${ data } is an invalid type`, 161 | too_big: ( { maximum } ) => `Maximum length is ${ maximum }`, 162 | invalid_enum_value: ( { data, options } ) => 163 | `${ data } is not a valid enum value. Valid options: ${ options?.join( ' | ' ) } `, 164 | } satisfies Parameters[ 0 ] 165 | 166 | const errorMap = zu.makeErrorMap( config ) 167 | 168 | const maximum = 32 169 | const stringSchema = z.string( { errorMap } ).max( maximum ) 170 | 171 | expect( zu.SPR( stringSchema.safeParse( undefined ) ).error?.issues[ 0 ].message ) 172 | .toBe( config.required ) 173 | 174 | expect( zu.SPR( stringSchema.safeParse( 42 ) ).error?.issues[ 0 ].message ) 175 | .toBe( 176 | config.invalid_type( { 177 | ...errorCtxMock, 178 | data: 42 179 | } ) 180 | ) 181 | 182 | expect( 183 | zu.SPR( 184 | stringSchema.safeParse( 'this string is over the maximum length' ) 185 | ).error?.issues[ 0 ].message 186 | ).toBe( 187 | config.too_big( { 188 | ...errorCtxMock, 189 | code: 'too_big', 190 | type: 'string', 191 | maximum, 192 | inclusive: true, 193 | } ) 194 | ) 195 | 196 | const enumSchema = z.enum( [ 'foo', 'bar' ], { errorMap } ) 197 | 198 | expect( zu.SPR( enumSchema.safeParse( 'baz' ) ).error?.issues[ 0 ].message ) 199 | .toBe( 200 | config.invalid_enum_value( { 201 | ...errorCtxMock, 202 | data: 'baz', 203 | code: 'invalid_enum_value', 204 | options: enumSchema.options 205 | } ) 206 | ) 207 | } ) -------------------------------------------------------------------------------- /src/useFormLike.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { z } from 'zod' 3 | import { zu } from '.' 4 | 5 | const schema = z.object( { 6 | manyStrings: zu.coerce( z.string().array().min( 1 ) ), 7 | manyNumbers: zu.coerce( z.number().array().min( 1 ) ), 8 | oneStringInArray: zu.coerce( z.string().array().length( 1 ) ), 9 | oneNumberInArray: zu.coerce( z.number().array().length( 1 ) ), 10 | stringMin1: z.string().min( 1 ), 11 | posNumber: z.number().positive(), 12 | range: z.number().min( 0 ).max( 5 ), 13 | boolean: z.boolean(), 14 | true: z.literal( true ), 15 | false: z.literal( false ), 16 | null: z.null(), 17 | date: z.coerce.date(), 18 | plainDate: z.string().refine( 19 | value => /\d{4}-\d{2}-\d{2}/.test( value ), 20 | value => ( { message: `Invalid plain date: ${ value }` } ), 21 | ), 22 | tuple: z.tuple( [ z.string(), z.number() ] ), 23 | object: z.object( { 24 | foo: z.string(), 25 | bar: z.number(), 26 | } ), 27 | } ) 28 | 29 | const searchParamsSchema = zu.useURLSearchParams( schema ) 30 | 31 | test( 'useURLSearchParams happy path', () => { 32 | 33 | const searchParams = new URLSearchParams( { 34 | oneStringInArray: 'Leeeeeeeeeroyyyyyyy Jenkiiiiiins!', 35 | oneNumberInArray: '42', 36 | boolean: 'true', 37 | true: 'true', 38 | false: 'false', 39 | stringMin1: 'foo', 40 | posNumber: '42.42', 41 | range: '4', 42 | null: 'null', 43 | date: '2023-01-01', 44 | plainDate: '2023-01-01', 45 | tuple: '["foo",42]', 46 | object: '{"foo":"foo","bar":42}', 47 | } ) 48 | 49 | searchParams.append( 'manyStrings', 'hello' ) 50 | searchParams.append( 'manyStrings', 'world' ) 51 | searchParams.append( 'manyNumbers', '123' ) 52 | searchParams.append( 'manyNumbers', '456' ) 53 | 54 | const result = zu.SPR( searchParamsSchema.safeParse( searchParams ) ) 55 | 56 | expect( result.data ).toMatchObject( { 57 | manyStrings: [ 'hello', 'world' ], 58 | manyNumbers: [ 123, 456 ], 59 | oneStringInArray: [ 'Leeeeeeeeeroyyyyyyy Jenkiiiiiins!' ], 60 | oneNumberInArray: [ 42 ], 61 | stringMin1: 'foo', 62 | posNumber: 42.42, 63 | range: 4, 64 | boolean: true, 65 | true: true, 66 | false: false, 67 | null: null, 68 | date: new Date( '2023-01-01' ), 69 | plainDate: '2023-01-01', 70 | tuple: [ 'foo', 42 ], 71 | object: { foo: 'foo', bar: 42 } 72 | } ) 73 | } ) 74 | 75 | test( 'useURLSearchParams sad path', () => { 76 | 77 | const searchParams = new URLSearchParams( { 78 | manyStrings: 'hello', 79 | manyNumbers: '123', 80 | oneStringInArray: 'Leeeeeeeeeroyyyyyyy', 81 | oneNumberInArray: 'foo', 82 | boolean: 'foo', 83 | true: 'foo', 84 | false: 'foo', 85 | stringMin1: '', 86 | posNumber: '-42', 87 | range: '6', 88 | null: 'undefined', 89 | date: '0000-00-00', 90 | plainDate: '0000-00-00', 91 | tuple: '[42,"foo"]', 92 | object: '{"foo":42,"bar":"foo"}', 93 | } ) 94 | 95 | searchParams.append( 'oneStringInArray', 'Jenkiiiiiins!' ) 96 | 97 | const result = zu.SPR( searchParamsSchema.safeParse( searchParams ) ) 98 | 99 | expect( result.error?.flatten() ).toMatchObject( 100 | { 101 | formErrors: [], 102 | fieldErrors: { 103 | oneStringInArray: [ 'Array must contain exactly 1 element(s)' ], 104 | oneNumberInArray: [ 'Expected number, received nan' ], 105 | stringMin1: [ 'String must contain at least 1 character(s)' ], 106 | posNumber: [ 'Number must be greater than 0' ], 107 | range: [ 'Number must be less than or equal to 5' ], 108 | boolean: [ 'Expected boolean, received string' ], 109 | true: [ 'Invalid literal value, expected true' ], 110 | false: [ 'Invalid literal value, expected false' ], 111 | null: [ 'Expected null, received string' ], 112 | date: [ 'Invalid date' ], 113 | tuple: [ 'Expected string, received number', 'Expected number, received string' ], 114 | object: [ 'Expected string, received number', 'Expected number, received string' ] 115 | } 116 | } as any 117 | ) 118 | } ) 119 | 120 | test( 'passthrough', () => { 121 | const objSchema = z.object( { 122 | string: z.string(), 123 | number: z.number(), 124 | boolean: z.boolean(), 125 | } ).passthrough() 126 | 127 | const schema = zu.useURLSearchParams( objSchema ) 128 | 129 | expect( 130 | zu.SPR( schema.safeParse( 131 | new URLSearchParams( { 132 | string: 'foo', 133 | number: '42', 134 | boolean: 'false', 135 | extraKey: 'extraValue', 136 | } ) 137 | ) ).data 138 | ).toMatchObject( 139 | { 140 | string: 'foo', 141 | number: 42, 142 | boolean: false, 143 | extraKey: 'extraValue' 144 | } as any 145 | ) 146 | } ) 147 | 148 | test( 'strict', () => { 149 | const objSchema = z.object( {} ).strict() 150 | const schema = zu.useURLSearchParams( objSchema ) 151 | expect( 152 | zu.SPR( schema.safeParse( 153 | new URLSearchParams( { 154 | extraKey: 'extraValue', 155 | } ) 156 | ) ).error?.flatten().formErrors 157 | ).toMatchObject( 158 | [ "Unrecognized key(s) in object: 'extraKey'" ] 159 | ) 160 | } 161 | ) 162 | 163 | test( 'README Example', () => { 164 | const schema = zu.useURLSearchParams( 165 | z.object( { 166 | string: z.string(), 167 | number: z.number(), 168 | boolean: z.boolean(), 169 | } ) 170 | ) 171 | 172 | expect( 173 | zu.SPR( schema.safeParse( 174 | new URLSearchParams( { 175 | string: 'foo', 176 | number: '42', 177 | boolean: 'false', 178 | } ) 179 | ) ).data 180 | ).toMatchObject( 181 | { string: 'foo', number: 42, boolean: false } 182 | ) 183 | 184 | expect( 185 | zu.SPR( schema.safeParse( 186 | new URLSearchParams( { 187 | string: '42', 188 | number: 'false', 189 | boolean: 'foo', 190 | } ) 191 | ) ).error?.flatten().fieldErrors 192 | ).toMatchObject( 193 | { 194 | string: [ 'Expected string, received number' ], 195 | number: [ 'Expected number, received boolean' ], 196 | boolean: [ 'Expected boolean, received string' ], 197 | } as any 198 | ) 199 | } ) 200 | 201 | test( 'README Example', () => { 202 | const schema = zu.useFormData( 203 | z.object( { 204 | string: z.string(), 205 | number: z.number(), 206 | boolean: z.boolean(), 207 | file: z.instanceof( File ), 208 | } ) 209 | ) 210 | 211 | { 212 | const file = new File( [], 'filename.ext' ) 213 | const formData = new FormData() 214 | formData.append( 'string', 'foo' ) 215 | formData.append( 'number', '42' ) 216 | formData.append( 'boolean', 'false' ) 217 | formData.append( 'file', file ) 218 | 219 | expect( 220 | zu.SPR( schema.safeParse( formData ) ).data 221 | ).toMatchObject( 222 | { string: 'foo', number: 42, boolean: false, file } 223 | ) 224 | } 225 | 226 | { 227 | const formData = new FormData() 228 | formData.append( 'string', '42' ) 229 | formData.append( 'number', 'false' ) 230 | formData.append( 'boolean', 'foo' ) 231 | formData.append( 'file', 'filename.ext' ) 232 | 233 | expect( 234 | zu.SPR( schema.safeParse( formData ) ).error?.flatten().fieldErrors 235 | ).toMatchObject( 236 | { 237 | string: [ 'Expected string, received number' ], 238 | number: [ 'Expected number, received boolean' ], 239 | boolean: [ 'Expected boolean, received string' ], 240 | file: [ 'Input not instance of File' ], 241 | } as any 242 | ) 243 | } 244 | } ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Zod Utilz logo 3 |

Zod Utilz

4 |

5 | Framework agnostic utilities for 6 | 7 | Zod 8 | 9 |

10 |
11 | 12 | 21 | 22 | 30 | 31 |
32 | Docs 33 |   •   34 | github 35 |   •   36 | npm 37 |
38 | 39 | 40 | 41 |
42 | 43 | ## Table of contents 44 | - [Purpose](#purpose) 45 | - [Contribute](#contribute) 46 | - [Yet another library](#yet-another-library) 47 | - [Installation](#installation) 48 | - [From npm (Node/Bun)](#from-npm-nodebun) 49 | - [Getting Started](#getting-started) 50 | - [import](#import) 51 | - [Utilz](#utilz) 52 | - [SPR (SafeParseResult)](#spr) 53 | - [makeErrorMap](#makeerrormap) 54 | - [useTypedParsers](#usetypedparsers) 55 | - [coerce](#coerce) 56 | - [useURLSearchParams](#useurlsearchparams) 57 | - [useFormData](#useformdata) 58 | - [partialSafeParse](#partialsafeparse) 59 | - [json](#json) 60 | - [stringToJSON](#stringtojson) 61 | - [TODO](#todo) 62 | 63 | ## Purpose 64 | - Simplify common tasks in [Zod](https://github.com/colinhacks/zod) 65 | - Fill the gap of features that might be missing in [Zod](https://github.com/colinhacks/zod) 66 | - Provide implementations for potential new features in [Zod](https://github.com/colinhacks/zod) 67 | 68 | ## Contribute 69 | Always open to ideas. Positive or negative, all are welcome. Feel free to contribute an [issue](https://github.com/JacobWeisenburger/zod_utilz/issues) or [PR](https://github.com/JacobWeisenburger/zod_utilz/pulls). 70 | 71 | ## Yet another library 72 | You might not want to install yet another library only to get access to that one [Util](#utilz) you need. No worries. Feel free to copy and paste the code you need into your project. It won't get updated when this library gets updated, but it will reduce your bundle size. :D 73 | 74 | Perhaps in the future there will be a way to install only the [Utilz](#utilz) you need. If you know how to do this, please [let me know](https://github.com/JacobWeisenburger/zod_utilz/issues). 75 | 76 | ## Installation 77 | 78 | ### From npm (Node/Bun) 79 | ```sh 80 | npm install zod_utilz 81 | yarn add zod_utilz 82 | pnpm add zod_utilz 83 | bun add zod_utilz 84 | ``` 85 | 86 | ## Getting Started 87 | 88 | ### import 89 | #### [Node/Bun](https://www.npmjs.com/package/zod_utilz) 90 | ```ts 91 | import { zu } from 'zod_utilz' 92 | ``` 93 | 94 | #### [Deno](https://deno.land/x/zod_utilz) 95 | ```ts 96 | import { zu } from 'npm:zod_utilz' 97 | ``` 98 | 99 | ## Utilz 100 | 101 | ### SPR 102 | SPR stands for SafeParseResult 103 | 104 | This enables [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) or [nullish coalescing](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing) for `z.SafeParseReturnType`. 105 | 106 | ```ts 107 | import { zu } from 'zod_utilz' 108 | const schema = z.object( { foo: z.string() } ) 109 | const result = zu.SPR( schema.safeParse( { foo: 42 } ) ) 110 | const fooDataOrErrors = result.data?.foo ?? result.error?.format().foo?._errors 111 | ``` 112 | 113 | ### makeErrorMap 114 | Simplifies the process of making a `ZodErrorMap` 115 | ```ts 116 | import { zu } from 'zod_utilz' 117 | 118 | const errorMap = zu.makeErrorMap( { 119 | required: 'Custom required message', 120 | invalid_type: ( { data } ) => `${ data } is an invalid type`, 121 | too_big: ( { maximum } ) => `Maximum length is ${ maximum }`, 122 | invalid_enum_value: ( { data, options } ) => 123 | `${ data } is not a valid enum value. Valid options: ${ options?.join( ' | ' ) } `, 124 | } ) 125 | 126 | const stringSchema = z.string( { errorMap } ).max( 32 ) 127 | 128 | zu.SPR( stringSchema.safeParse( undefined ) ).error?.issues[ 0 ].message 129 | // Custom required message 130 | 131 | zu.SPR( stringSchema.safeParse( 42 ) ).error?.issues[ 0 ].message 132 | // 42 is an invalid type 133 | 134 | zu.SPR( stringSchema.safeParse( 'this string is over the maximum length' ) ).error?.issues[ 0 ].message 135 | // Maximum length is 32 136 | 137 | const enumSchema = z.enum( [ 'foo', 'bar' ], { errorMap } ) 138 | 139 | zu.SPR( enumSchema.safeParse( 'baz' ) ).error?.issues[ 0 ].message 140 | // baz is not a valid enum value. Valid options: foo | bar 141 | ``` 142 | 143 | ### useTypedParsers 144 | Enables compile time type checking for zod parsers. 145 | ```ts 146 | import { zu } from 'zod_utilz' 147 | const schemaWithTypedParsers = zu.useTypedParsers( z.literal( 'foo' ) ) 148 | 149 | schemaWithTypedParsers.parse( 'foo' ) 150 | // no ts errors 151 | 152 | schemaWithTypedParsers.parse( 'bar' ) 153 | // ^^^^^ 154 | // Argument of type '"bar"' is not assignable to parameter of type '"foo"' 155 | ``` 156 | 157 | ### coerce 158 | Coercion that treats errors like normal zod errors. Prevents throwing errors when using `safeParse`. 159 | 160 | #### z.bigint() 161 | ```ts 162 | import { zu } from 'zod_utilz' 163 | const bigintSchema = zu.coerce( z.bigint() ) 164 | bigintSchema.parse( '42' ) // 42n 165 | bigintSchema.parse( '42n' ) // 42n 166 | zu.SPR( bigintSchema.safeParse( 'foo' ) ).error?.issues[ 0 ].message 167 | // 'Expected bigint, received string' 168 | ``` 169 | 170 | #### z.boolean() 171 | ```ts 172 | import { zu } from 'zod_utilz' 173 | const booleanSchema = zu.coerce( z.boolean() ) 174 | 175 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean 176 | // only exception to normal boolean coercion rules 177 | booleanSchema.parse( 'false' ) // false 178 | 179 | // https://developer.mozilla.org/en-US/docs/Glossary/Falsy 180 | // falsy => false 181 | booleanSchema.parse( false ) // false 182 | booleanSchema.parse( 0 ) // false 183 | booleanSchema.parse( -0 ) // false 184 | booleanSchema.parse( 0n ) // false 185 | booleanSchema.parse( '' ) // false 186 | booleanSchema.parse( null ) // false 187 | booleanSchema.parse( undefined ) // false 188 | booleanSchema.parse( NaN ) // false 189 | 190 | // truthy => true 191 | booleanSchema.parse( 'foo' ) // true 192 | booleanSchema.parse( 42 ) // true 193 | booleanSchema.parse( [] ) // true 194 | booleanSchema.parse( {} ) // true 195 | ``` 196 | 197 | #### z.number().array() 198 | ```ts 199 | import { zu } from 'zod_utilz' 200 | const numberArraySchema = zu.coerce( z.number().array() ) 201 | 202 | // if the value is not an array, it is coerced to an array with one coerced item 203 | numberArraySchema.parse( 42 ) // [ 42 ] 204 | numberArraySchema.parse( '42' ) // [ 42 ] 205 | 206 | // if the value is an array, it coerces each item in the array 207 | numberArraySchema.parse( [] ) // [] 208 | numberArraySchema.parse( [ '42', 42 ] ) // [ 42, 42 ] 209 | 210 | zu.SPR( numberArraySchema.safeParse( 'foo' ) ).error?.issues[ 0 ].message 211 | // 'Expected number, received nan' 212 | ``` 213 | 214 | ### useURLSearchParams 215 | A way to parse URLSearchParams 216 | ```ts 217 | import { zu } from 'zod_utilz' 218 | const schema = zu.useURLSearchParams( 219 | z.object( { 220 | string: z.string(), 221 | number: z.number(), 222 | boolean: z.boolean(), 223 | } ) 224 | ) 225 | 226 | zu.SPR( schema.safeParse( 227 | new URLSearchParams( { 228 | string: 'foo', 229 | number: '42', 230 | boolean: 'false', 231 | } ) 232 | ) ).data 233 | // { string: 'foo', number: 42, boolean: false } 234 | 235 | zu.SPR( schema.safeParse( 236 | new URLSearchParams( { 237 | string: '42', 238 | number: 'false', 239 | boolean: 'foo', 240 | } ) 241 | ) ).error?.flatten().fieldErrors 242 | // { 243 | // string: [ 'Expected string, received number' ], 244 | // number: [ 'Expected number, received boolean' ], 245 | // boolean: [ 'Expected boolean, received string' ], 246 | // } 247 | ``` 248 | 249 | ### useFormData 250 | A way to parse FormData 251 | ```ts 252 | import { zu } from 'zod_utilz' 253 | const schema = zu.useFormData( 254 | z.object( { 255 | string: z.string(), 256 | number: z.number(), 257 | boolean: z.boolean(), 258 | file: z.instanceof( File ), 259 | } ) 260 | ) 261 | ``` 262 | ```ts 263 | const formData = new FormData() 264 | formData.append( 'string', 'foo' ) 265 | formData.append( 'number', '42' ) 266 | formData.append( 'boolean', 'false' ) 267 | formData.append( 'file', new File( [], 'filename.ext' ) ) 268 | 269 | zu.SPR( schema.safeParse( formData ) ).data, 270 | // { string: 'foo', number: 42, boolean: false, file: File } 271 | ``` 272 | ```ts 273 | const formData = new FormData() 274 | formData.append( 'string', '42' ) 275 | formData.append( 'number', 'false' ) 276 | formData.append( 'boolean', 'foo' ) 277 | formData.append( 'file', 'filename.ext' ) 278 | 279 | zu.SPR( schema.safeParse( formData ) ).error?.flatten().fieldErrors, 280 | // { 281 | // string: [ 'Expected string, received number' ], 282 | // number: [ 'Expected number, received boolean' ], 283 | // boolean: [ 'Expected boolean, received string' ], 284 | // file: [ 'Input not instance of File' ], 285 | // } 286 | ``` 287 | 288 | ### partialSafeParse 289 | partialSafeParse allows you to get the valid fields even if there was an error in another field 290 | ```ts 291 | import { zu } from 'zod_utilz' 292 | const userSchema = z.object( { name: z.string(), age: z.number() } ) 293 | const result = zu.partialSafeParse( userSchema, { name: null, age: 42 } ) 294 | // { 295 | // successType: 'partial', 296 | // validData: { age: 42 }, 297 | // invalidData: { name: null }, 298 | // } 299 | result.error?.flatten().fieldErrors 300 | // { name: [ 'Expected string, received null' ] } 301 | ``` 302 | 303 | ### json 304 | zu.json() is a schema that validates that a JavaScript object is JSON-compatible. This includes `string`, `number`, `boolean`, and `null`, plus `Array`s and `Object`s containing JSON-compatible types as values 305 | ```ts 306 | import { zu } from 'zod_utilz' 307 | const schema = zu.json() 308 | schema.parse( false ) // false 309 | schema.parse( 8675309 ) // 8675309 310 | schema.parse( { a: 'deeply', nested: [ 'JSON', 'object' ] } ) 311 | // { a: 'deeply', nested: [ 'JSON', 'object' ] } 312 | ``` 313 | 314 | ### stringToJSON 315 | zu.stringToJSON() is a schema that validates JSON encoded as a string, then returns the parsed value 316 | ```ts 317 | import { zu } from 'zod_utilz' 318 | const schema = zu.stringToJSON() 319 | schema.parse( 'true' ) // true 320 | schema.parse( 'null' ) // null 321 | schema.parse( '["one", "two", "three"]' ) // ['one', 'two', 'three'] 322 | schema.parse( 'not a JSON string' ) // throws 323 | ``` 324 | 325 | ## TODO 326 | Always open to ideas. Positive or negative, all are welcome. Feel free to contribute an [issue](https://github.com/JacobWeisenburger/zod_utilz/issues) or [PR](https://github.com/JacobWeisenburger/zod_utilz/pulls). 327 | - tests for `lib/mapValues` 328 | - Shrink Bundle Size 329 | - tree-shaking deps 330 | - lodash 331 | - zu.coerce 332 | - z.date() 333 | - z.object() 334 | - recursively coerce props 335 | - https://github.com/colinhacks/zod/discussions/1910 336 | - enum pick/omit 337 | - https://github.com/colinhacks/zod/discussions/1922 338 | - BaseType (Recursively get the base type of a Zod type) 339 | - zu.baseType( z.string() ) => z.string() 340 | - zu.baseType( z.string().optional() ) => z.string() 341 | - zu.baseType( z.string().optional().refine() ) => z.string() 342 | - zu.baseType( z.string().array().optional().refine() ) => z.string().array() 343 | - Make process for minifying 344 | - GitHub Actions 345 | - Auto publish to npm -------------------------------------------------------------------------------- /src/coerce.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { z } from 'zod' 3 | import { zu } from '.' 4 | 5 | test( 'coerce( z.string() )', () => { 6 | const schema = zu.coerce( z.string() ) 7 | 8 | /* primitives */ 9 | expect( schema.parse( 'foo' ) ).toBe( 'foo' ) 10 | expect( schema.parse( 42 ) ).toBe( '42' ) 11 | expect( schema.parse( 42n ) ).toBe( '42' ) 12 | expect( schema.parse( true ) ).toBe( 'true' ) 13 | expect( schema.parse( false ) ).toBe( 'false' ) 14 | expect( schema.parse( undefined ) ).toBe( 'undefined' ) 15 | expect( schema.parse( Symbol( 'symbol' ) ) ).toBe( 'Symbol(symbol)' ) 16 | expect( schema.parse( null ) ).toBe( 'null' ) 17 | 18 | /* objects */ 19 | expect( schema.parse( {} ) ).toBe( '{}' ) 20 | expect( schema.parse( { foo: 'foo' } ) ).toBe( '{"foo":"foo"}' ) 21 | expect( schema.parse( { foo: 'foo', bar: 'bar' } ) ).toBe( '{"foo":"foo","bar":"bar"}' ) 22 | expect( schema.parse( [] ) ).toBe( '[]' ) 23 | expect( schema.parse( [ 'foo' ] ) ).toBe( '["foo"]' ) 24 | expect( schema.parse( [ 'foo', 'bar' ] ) ).toBe( '["foo","bar"]' ) 25 | expect( schema.parse( () => { } ) ).toBeTruthy() 26 | expect( schema.parse( ( arg: any ) => { } ) ).toBeTruthy() 27 | expect( schema.parse( ( arg1: any, arg2: any ) => { } ) ).toBeTruthy() 28 | } ) 29 | 30 | test( 'coerce( z.number() )', () => { 31 | const schema = zu.coerce( z.number() ) 32 | 33 | /* primitives */ 34 | expect( () => schema.parse( 'foo' ) ).toThrow() 35 | expect( schema.parse( '42' ) ).toBe( 42 ) 36 | expect( schema.parse( 42 ) ).toBe( 42 ) 37 | expect( schema.parse( 42n ) ).toBe( 42 ) 38 | expect( schema.parse( true ) ).toBe( 1 ) 39 | expect( schema.parse( false ) ).toBe( 0 ) 40 | expect( () => schema.parse( undefined ) ).toThrow() 41 | expect( () => schema.parse( Symbol( 'symbol' ) ) ).toThrow() 42 | expect( () => schema.parse( null ) ).toThrow() 43 | 44 | /* objects */ 45 | expect( () => schema.parse( {} ) ).toThrow() 46 | expect( () => schema.parse( { foo: 'foo' } ) ).toThrow() 47 | expect( () => schema.parse( { foo: 'foo', bar: 'bar' } ) ).toThrow() 48 | expect( () => schema.parse( [] ) ).toThrow() 49 | expect( () => schema.parse( [ 'foo' ] ) ).toThrow() 50 | expect( () => schema.parse( [ 'foo', 'bar' ] ) ).toThrow() 51 | expect( () => schema.parse( () => { } ) ).toThrow() 52 | expect( () => schema.parse( ( arg: any ) => { } ) ).toThrow() 53 | expect( () => schema.parse( ( arg1: any, arg2: any ) => { } ) ).toThrow() 54 | } ) 55 | 56 | test( 'coerce( z.bigint() )', () => { 57 | const schema = zu.coerce( z.bigint() ) 58 | 59 | /* primitives */ 60 | expect( () => schema.parse( 'foo' ) ).toThrow() 61 | expect( schema.parse( '42' ) ).toBe( 42n ) 62 | expect( schema.parse( '42n' ) ).toBe( 42n ) 63 | expect( schema.parse( 42 ) ).toBe( 42n ) 64 | expect( schema.parse( 42n ) ).toBe( 42n ) 65 | expect( schema.parse( true ) ).toBe( 1n ) 66 | expect( schema.parse( false ) ).toBe( 0n ) 67 | expect( () => schema.parse( undefined ) ).toThrow() 68 | expect( () => schema.parse( Symbol( 'symbol' ) ) ).toThrow() 69 | expect( () => schema.parse( null ) ).toThrow() 70 | 71 | /* objects */ 72 | expect( () => schema.parse( {} ) ).toThrow() 73 | expect( () => schema.parse( { foo: 'foo' } ) ).toThrow() 74 | expect( () => schema.parse( { foo: 'foo', bar: 'bar' } ) ).toThrow() 75 | expect( () => schema.parse( [] ) ).toThrow() 76 | expect( () => schema.parse( [ 'foo' ] ) ).toThrow() 77 | expect( () => schema.parse( [ 'foo', 'bar' ] ) ).toThrow() 78 | expect( () => schema.parse( () => { } ) ).toThrow() 79 | expect( () => schema.parse( ( arg: any ) => { } ) ).toThrow() 80 | expect( () => schema.parse( ( arg1: any, arg2: any ) => { } ) ).toThrow() 81 | } ) 82 | 83 | test( 'coerce( z.boolean() )', () => { 84 | const schema = zu.coerce( z.boolean() ) 85 | 86 | /* primitives */ 87 | expect( schema.parse( 'false' ) ).toBe( false ) 88 | expect( schema.parse( 'true' ) ).toBe( true ) 89 | expect( schema.parse( 'foo' ) ).toBe( true ) 90 | expect( schema.parse( 0 ) ).toBe( false ) 91 | expect( schema.parse( 42 ) ).toBe( true ) 92 | expect( schema.parse( 42n ) ).toBe( true ) 93 | expect( schema.parse( true ) ).toBe( true ) 94 | expect( schema.parse( false ) ).toBe( false ) 95 | expect( schema.parse( undefined ) ).toBe( false ) 96 | expect( schema.parse( Symbol( 'symbol' ) ) ).toBe( true ) 97 | expect( schema.parse( null ) ).toBe( false ) 98 | 99 | /* objects */ 100 | expect( schema.parse( {} ) ).toBe( true ) 101 | expect( schema.parse( { foo: 'foo' } ) ).toBe( true ) 102 | expect( schema.parse( { foo: 'foo', bar: 'bar' } ) ).toBe( true ) 103 | expect( schema.parse( [] ) ).toBe( true ) 104 | expect( schema.parse( [ 'foo' ] ) ).toBe( true ) 105 | expect( schema.parse( [ 'foo', 'bar' ] ) ).toBe( true ) 106 | expect( schema.parse( () => { } ) ).toBe( true ) 107 | expect( schema.parse( ( arg: any ) => { } ) ).toBe( true ) 108 | expect( schema.parse( ( arg1: any, arg2: any ) => { } ) ).toBe( true ) 109 | } ) 110 | 111 | test( 'coerce( z.any().array() )', () => { 112 | const schema = zu.coerce( z.any().array() ) 113 | 114 | /* primitives */ 115 | expect( schema.parse( 'foo' ) ).toEqual( [ 'foo' ] ) 116 | expect( schema.parse( 42 ) ).toEqual( [ 42 ] ) 117 | expect( schema.parse( 42n ) ).toEqual( [ 42n ] ) 118 | expect( schema.parse( true ) ).toEqual( [ true ] ) 119 | expect( schema.parse( false ) ).toEqual( [ false ] ) 120 | expect( schema.parse( undefined ) ).toEqual( [ undefined ] ) 121 | expect( schema.parse( null ) ).toEqual( [ null ] ) 122 | 123 | const symbol = Symbol( 'symbol' ) 124 | expect( schema.parse( symbol ) ).toEqual( [ symbol ] ) 125 | 126 | /* objects */ 127 | expect( schema.parse( {} ) ).toEqual( [ {} ] ) 128 | expect( schema.parse( { foo: 'foo' } ) ).toEqual( [ { foo: 'foo' } ] ) 129 | expect( schema.parse( { foo: 'foo', bar: 'bar' } ) ).toEqual( [ { foo: 'foo', bar: 'bar' } ] ) 130 | expect( schema.parse( [] ) ).toEqual( [] ) 131 | expect( schema.parse( [ 'foo' ] ) ).toEqual( [ 'foo' ] ) 132 | expect( schema.parse( [ 'foo', 'bar' ] ) ).toEqual( [ 'foo', 'bar' ] ) 133 | 134 | const fn1 = () => { } 135 | expect( schema.parse( fn1 ) ).toEqual( [ fn1 ] ) 136 | 137 | const fn2 = ( arg: any ) => { } 138 | expect( schema.parse( fn2 ) ).toEqual( [ fn2 ] ) 139 | 140 | const fn3 = ( arg1: any, arg2: any ) => { } 141 | expect( schema.parse( fn3 ) ).toEqual( [ fn3 ] ) 142 | } ) 143 | 144 | test( 'coerce( z.string().array() )', () => { 145 | const schema = zu.coerce( z.string().array() ) 146 | 147 | /* primitives */ 148 | expect( schema.parse( 'foo' ) ).toMatchObject( [ 'foo' ] ) 149 | expect( schema.parse( 42 ) ).toMatchObject( [ '42' ] ) 150 | expect( schema.parse( 42n ) ).toMatchObject( [ '42' ] ) 151 | expect( schema.parse( true ) ).toMatchObject( [ 'true' ] ) 152 | expect( schema.parse( false ) ).toMatchObject( [ 'false' ] ) 153 | expect( schema.parse( undefined ) ).toMatchObject( [ 'undefined' ] ) 154 | expect( schema.parse( null ) ).toMatchObject( [ 'null' ] ) 155 | 156 | const symbol = Symbol( 'symbol' ) 157 | expect( schema.parse( symbol ) ).toMatchObject( [ symbol.toString() ] ) 158 | 159 | /* objects */ 160 | expect( schema.parse( {} ) ).toMatchObject( [ '{}' ] ) 161 | expect( schema.parse( { foo: 'foo' } ) ).toMatchObject( [ '{"foo":"foo"}' ] ) 162 | expect( schema.parse( { foo: 'foo', bar: 'bar' } ) ).toMatchObject( [ '{"foo":"foo","bar":"bar"}' ] ) 163 | expect( schema.parse( [] ) ).toMatchObject( [] ) 164 | expect( schema.parse( [ 'foo' ] ) ).toMatchObject( [ 'foo' ] ) 165 | expect( schema.parse( [ 'foo', 'bar' ] ) ).toMatchObject( [ 'foo', 'bar' ] ) 166 | expect( schema.parse( [ 'foo', '42' ] ) ).toMatchObject( [ 'foo', '42' ] ) 167 | expect( schema.parse( [ '42', '42' ] ) ).toMatchObject( [ '42', '42' ] ) 168 | expect( schema.parse( () => { } ) ).toBeTruthy() 169 | expect( schema.parse( ( arg: any ) => { } ) ).toBeTruthy() 170 | expect( schema.parse( ( arg1: any, arg2: any ) => { } ) ).toBeTruthy() 171 | } ) 172 | 173 | test( 'coerce( z.number().array() )', () => { 174 | const schema = zu.coerce( z.number().array() ) 175 | 176 | /* primitives */ 177 | expect( () => schema.parse( 'foo' ) ).toThrow() 178 | expect( schema.parse( '42' ) ).toMatchObject( [ 42 ] ) 179 | expect( schema.parse( 42 ) ).toMatchObject( [ 42 ] ) 180 | expect( schema.parse( 42n ) ).toMatchObject( [ 42 ] ) 181 | expect( schema.parse( true ) ).toMatchObject( [ 1 ] ) 182 | expect( schema.parse( false ) ).toMatchObject( [ 0 ] ) 183 | expect( () => schema.parse( undefined ) ).toThrow() 184 | expect( () => schema.parse( Symbol( 'symbol' ) ) ).toThrow() 185 | expect( () => schema.parse( null ) ).toThrow() 186 | 187 | /* objects */ 188 | expect( () => schema.parse( {} ) ).toThrow() 189 | expect( () => schema.parse( { foo: 'foo' } ) ).toThrow() 190 | expect( () => schema.parse( { foo: 'foo', bar: 'bar' } ) ).toThrow() 191 | expect( schema.parse( [] ) ).toEqual( [] ) 192 | expect( () => schema.parse( [ 'foo' ] ) ).toThrow() 193 | expect( () => schema.parse( [ 'foo', 'bar' ] ) ).toThrow() 194 | expect( () => schema.parse( [ 'foo', '42' ] ) ).toThrow() 195 | expect( schema.parse( [ '42', '42' ] ) ).toEqual( [ 42, 42 ] ) 196 | expect( () => schema.parse( () => { } ) ).toThrow() 197 | expect( () => schema.parse( ( arg: any ) => { } ) ).toThrow() 198 | expect( () => schema.parse( ( arg1: any, arg2: any ) => { } ) ).toThrow() 199 | } ) 200 | 201 | test( 'coerce( z.bigint().array() )', () => { 202 | const schema = zu.coerce( z.bigint().array() ) 203 | 204 | /* primitives */ 205 | expect( () => schema.parse( 'foo' ) ).toThrow() 206 | expect( schema.parse( '42' ) ).toMatchObject( [ 42n ] ) 207 | expect( schema.parse( '42n' ) ).toMatchObject( [ 42n ] ) 208 | expect( schema.parse( 42 ) ).toMatchObject( [ 42n ] ) 209 | expect( schema.parse( 42n ) ).toMatchObject( [ 42n ] ) 210 | expect( schema.parse( true ) ).toMatchObject( [ 1n ] ) 211 | expect( schema.parse( false ) ).toMatchObject( [ 0n ] ) 212 | expect( () => schema.parse( undefined ) ).toThrow() 213 | expect( () => schema.parse( Symbol( 'symbol' ) ) ).toThrow() 214 | expect( () => schema.parse( null ) ).toThrow() 215 | 216 | /* objects */ 217 | expect( () => schema.parse( {} ) ).toThrow() 218 | expect( () => schema.parse( { foo: 'foo' } ) ).toThrow() 219 | expect( () => schema.parse( { foo: 'foo', bar: 'bar' } ) ).toThrow() 220 | expect( schema.parse( [] ) ).toMatchObject( [] ) 221 | expect( () => schema.parse( [ 'foo' ] ) ).toThrow() 222 | expect( () => schema.parse( [ 'foo', 'bar' ] ) ).toThrow() 223 | expect( () => schema.parse( [ 'foo', '42' ] ) ).toThrow() 224 | expect( schema.parse( [ '42', '42' ] ) ).toMatchObject( [ 42n, 42n ] ) 225 | expect( () => schema.parse( () => { } ) ).toThrow() 226 | expect( () => schema.parse( ( arg: any ) => { } ) ).toThrow() 227 | expect( () => schema.parse( ( arg1: any, arg2: any ) => { } ) ).toThrow() 228 | } ) 229 | 230 | test( 'coerce( z.boolean().array() )', () => { 231 | const schema = zu.coerce( z.boolean().array() ) 232 | 233 | /* primitives */ 234 | expect( schema.parse( 'false' ) ).toMatchObject( [ false ] ) 235 | expect( schema.parse( 'true' ) ).toMatchObject( [ true ] ) 236 | expect( schema.parse( 'foo' ) ).toMatchObject( [ true ] ) 237 | expect( schema.parse( 0 ) ).toMatchObject( [ false ] ) 238 | expect( schema.parse( 42 ) ).toMatchObject( [ true ] ) 239 | expect( schema.parse( 42n ) ).toMatchObject( [ true ] ) 240 | expect( schema.parse( true ) ).toMatchObject( [ true ] ) 241 | expect( schema.parse( false ) ).toMatchObject( [ false ] ) 242 | expect( schema.parse( undefined ) ).toMatchObject( [ false ] ) 243 | expect( schema.parse( Symbol( 'symbol' ) ) ).toMatchObject( [ true ] ) 244 | expect( schema.parse( null ) ).toMatchObject( [ false ] ) 245 | 246 | /* objects */ 247 | expect( schema.parse( {} ) ).toMatchObject( [ true ] ) 248 | expect( schema.parse( { foo: 'foo' } ) ).toMatchObject( [ true ] ) 249 | expect( schema.parse( { foo: 'foo', bar: 'bar' } ) ).toMatchObject( [ true ] ) 250 | expect( schema.parse( [] ) ).toMatchObject( [] ) 251 | expect( schema.parse( [ 'foo' ] ) ).toMatchObject( [ true ] ) 252 | expect( schema.parse( [ 'foo', 'bar' ] ) ).toMatchObject( [ true, true ] ) 253 | expect( schema.parse( [ 'foo', 'false' ] ) ).toMatchObject( [ true, false ] ) 254 | expect( schema.parse( [ 'false', 'false' ] ) ).toMatchObject( [ false, false ] ) 255 | expect( schema.parse( () => { } ) ).toMatchObject( [ true ] ) 256 | expect( schema.parse( ( arg: any ) => { } ) ).toMatchObject( [ true ] ) 257 | expect( schema.parse( ( arg1: any, arg2: any ) => { } ) ).toMatchObject( [ true ] ) 258 | } ) 259 | 260 | test( 'README Example: z.bigint()', () => { 261 | const bigintSchema = zu.coerce( z.bigint() ) 262 | expect( bigintSchema.parse( '42' ) ).toBe( 42n ) 263 | expect( bigintSchema.parse( '42n' ) ).toBe( 42n ) 264 | expect( zu.SPR( bigintSchema.safeParse( 'foo' ) ).error?.issues[ 0 ].message ) 265 | .toBe( 'Expected bigint, received string' ) 266 | } ) 267 | 268 | test( 'README Example: z.boolean()', () => { 269 | const booleanSchema = zu.coerce( z.boolean() ) 270 | 271 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean 272 | // only exception to normal boolean coercion rules 273 | expect( booleanSchema.parse( 'false' ) ).toBe( false ) 274 | 275 | // https://developer.mozilla.org/en-US/docs/Glossary/Falsy 276 | // falsy => false 277 | expect( booleanSchema.parse( false ) ).toBe( false ) 278 | expect( booleanSchema.parse( 0 ) ).toBe( false ) 279 | expect( booleanSchema.parse( -0 ) ).toBe( false ) 280 | expect( booleanSchema.parse( 0n ) ).toBe( false ) 281 | expect( booleanSchema.parse( '' ) ).toBe( false ) 282 | expect( booleanSchema.parse( null ) ).toBe( false ) 283 | expect( booleanSchema.parse( undefined ) ).toBe( false ) 284 | expect( booleanSchema.parse( NaN ) ).toBe( false ) 285 | 286 | // truthy => true 287 | expect( booleanSchema.parse( 'foo' ) ).toBe( true ) 288 | expect( booleanSchema.parse( 42 ) ).toBe( true ) 289 | expect( booleanSchema.parse( [] ) ).toBe( true ) 290 | expect( booleanSchema.parse( {} ) ).toBe( true ) 291 | } ) 292 | 293 | test( 'README Example: z.number().array()', () => { 294 | const numberArraySchema = zu.coerce( z.number().array() ) 295 | 296 | // if the value is not an array, it is coerced to an array with one coerced item 297 | expect( numberArraySchema.parse( 42 ) ).toMatchObject( [ 42 ] ) 298 | expect( numberArraySchema.parse( '42' ) ).toMatchObject( [ 42 ] ) 299 | 300 | // if the value is an array, it coerces each item in the array 301 | expect( numberArraySchema.parse( [] ) ).toMatchObject( [] ) 302 | expect( numberArraySchema.parse( [ '42', 42 ] ) ).toMatchObject( [ 42, 42 ] ) 303 | 304 | expect( zu.SPR( numberArraySchema.safeParse( 'foo' ) ).error?.issues[ 0 ].message ) 305 | .toBe( 'Expected number, received nan' ) 306 | } ) 307 | 308 | // TODO 309 | // test( 'README Example: z.date()', () => { 310 | // const schema = zu.coerce( z.date() ) 311 | // } ) 312 | 313 | // TODO 314 | // test( 'README Example: z.object()', () => { 315 | // const schema = zu.coerce( z.object() ) 316 | // } ) -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 9 | 10 | 11 | 14 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | tools 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 38 | 41 | 44 | 45 | 48 | 51 | 53 | 56 | 57 | 58 | 59 | --------------------------------------------------------------------------------