├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── checks.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── bun.lockb ├── package.json ├── src ├── arrays.ts ├── bigint.ts ├── coerce.ts ├── correttore.ts ├── dates.ts ├── index.ts ├── literal.ts ├── numbers.ts ├── object.ts ├── other.ts ├── primitives.ts ├── sets.ts ├── shared.types.ts ├── strings.ts ├── union.ts └── util.types.ts ├── tests ├── arrays.test.ts ├── coerce.test.ts ├── dates.test.ts ├── helpers.types.ts ├── index.test.ts ├── objects.test.ts └── union.test.ts ├── tsconfig.json └── tsup.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modueles -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 8 | "overrides": [], 9 | "parser": "@typescript-eslint/parser", 10 | "parserOptions": { 11 | "ecmaVersion": "latest", 12 | "sourceType": "module" 13 | }, 14 | "plugins": ["@typescript-eslint"], 15 | "rules": { 16 | "@typescript-eslint/no-explicit-any": "off", 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": [ 19 | "error", 20 | { 21 | "argsIgnorePattern": "^_", 22 | "varsIgnorePattern": "^_", 23 | "caughtErrorsIgnorePattern": "^_" 24 | } 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | checks: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up Bun 17 | uses: oven-sh/setup-bun@v1 18 | 19 | - name: Install dependencies 20 | run: bun install 21 | 22 | - name: Run check:format 23 | run: bun check:format 24 | 25 | - name: Run check:lint 26 | run: bun check:lint 27 | 28 | - name: Run check:types 29 | run: bun check:types 30 | 31 | - name: Run tests 32 | run: bun test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["Correttore"], 3 | "typescript.tsdk": "node_modules/typescript/lib" 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # correttore 2 | 3 | A **proof of concept** of a tree shakable [Zod](https://zod.dev/) alternative. 4 | This library aims to have a 1:1 Zod compatible API, but with fine grain control over the final bundle size. 5 | This was done by a combination of Proxies and type-level programming. 6 | 7 | 🤓 You can read the [blog post](https://softwaremill.com/a-novel-technique-for-creating-ergonomic-and-tree-shakable-typescript-libraries/) to learn how it works. 8 | 9 | ## Usage 10 | 11 | ```ts 12 | // 1. import `initCorrettore` and features (validator) you want to use 13 | import { initCorrettore, string, email, min, object } from "correttore"; // 0.54 kB 14 | 15 | // 2. init the entrypoint variable `c`. It corresponds to Zod's `z`. 16 | export const c = initCorrettore([string, email, min, object]); 17 | 18 | // 3. create schemas. Autocompletion will only show methods passed to `initCorrettore`. 19 | // the `object` method is always available 20 | const loginSchema = c.object({ 21 | email: c.string().email(), 22 | password: c.string().min(5), 23 | }); 24 | 25 | // 4. Infer type from schema: 26 | import type { Infer } from "correttore"; 27 | 28 | type LoginSchema = Infer; 29 | // ^? { 30 | // email: string; 31 | // password: string; 32 | // } 33 | 34 | // 5. Parse unknown data 35 | const parsed = loginSchema.parse({ 36 | email: "hello@test.com", 37 | password: "password123", 38 | }); 39 | 40 | // this will throw 41 | loginSchema.parse({ 42 | email: "hello@test.com", 43 | // missing field 44 | }); 45 | ``` 46 | 47 | ## Installation 48 | 49 | > **Note:** This library is not production ready, I implemented only a handful of parsers as a PoC. 50 | > If you're interested in this project, check out the [contributing](#contributing) section. 51 | 52 | ```sh 53 | # choose your package manager 54 | pnpm add correttore 55 | yarn add correttore 56 | npm install correttore 57 | ``` 58 | 59 | ## Comparison with Zod 60 | 61 | ### Zod: 62 | 63 | ```ts 64 | import { z } from "zod"; // 12.8 kB 65 | 66 | const LoginSchema = z.object({ 67 | email: z.string().email(), 68 | password: z.string().min(8), 69 | }); 70 | 71 | // Throws error 72 | LoginSchema.parse({ email: "", password: "" }); 73 | 74 | // Returns data as { email: string; password: string } 75 | LoginSchema.parse({ email: "jane@example.com", password: "12345678" }); 76 | ``` 77 | 78 | ### Correttore: 79 | 80 | ```ts 81 | import { email, min, initCorrettore, string, object } from "correttore"; // 0.54 kB 82 | 83 | export const c = initCorrettore([string, email, min, object]); 84 | 85 | const LoginSchema = c.object({ 86 | email: c.string().email(), 87 | password: c.string().min(8), 88 | }); 89 | 90 | // Throws error 91 | LoginSchema.parse({ email: "", password: "" }); 92 | 93 | // Returns data as { email: string; password: string } 94 | LoginSchema.parse({ email: "jane@example.com", password: "12345678" }); 95 | ``` 96 | 97 | ## Contributing 98 | 99 | If you're interested in helping to bring this project to a production ready state, feel free to open a PR with changes that will bring it closer to a 1:1 Zod compatible API. 100 | 101 | List of stuff to do: 102 | 103 | - [ ] Add more APIs (refer to the Roadmap (https://github.com/mieszkosabo/correttore/issues/2) to find out what needs to be done) 104 | - [ ] Add tests 105 | - [ ] Add docs/guides 106 | - [ ] Create premade "bundles" of popular subsets of APIs users may want to import 107 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mieszkosabo/correttore/68827236055d81d18ec4014980ca3a81d26a89c9/bun.lockb -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "correttore", 3 | "version": "0.1.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": { 11 | "types": "./dist/index.d.ts", 12 | "default": "./dist/index.js" 13 | }, 14 | "require": { 15 | "types": "./dist/index.d.cts", 16 | "default": "./dist/index.cjs" 17 | } 18 | } 19 | }, 20 | "scripts": { 21 | "build": "tsup", 22 | "clean": "rm -rf dist", 23 | "prettier:write": "prettier --write .", 24 | "test": "vitest run", 25 | "check:lint": "eslint .", 26 | "check:format": "prettier --check .", 27 | "check:types": "tsc --noEmit --pretty", 28 | "check:all": "bun check:lint && bun check:format && bun check:types && bun run test" 29 | }, 30 | "keywords": [], 31 | "author": "Mieszko Sabo", 32 | "sideEffects": false, 33 | "files": [ 34 | "dist" 35 | ], 36 | "license": "MIT", 37 | "devDependencies": { 38 | "@eslint/js": "^9.0.0", 39 | "@swc-node/register": "^1.6.6", 40 | "@swc/core": "^1.3.76", 41 | "@typescript-eslint/eslint-plugin": "latest", 42 | "@typescript-eslint/parser": "latest", 43 | "bun-types": "^1.1.3", 44 | "eslint": "8.57.0", 45 | "globals": "^15.0.0", 46 | "hotscript": "^1.0.13", 47 | "prettier": "^3.2.5", 48 | "tsup": "^7.2.0", 49 | "typescript": "5.3.3", 50 | "typescript-eslint": "^7.6.0", 51 | "vitest": "^1.5.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/arrays.ts: -------------------------------------------------------------------------------- 1 | import { Fn } from "hotscript"; 2 | import { Identity, Validator } from "./shared.types"; 3 | 4 | interface ArrayType extends Fn { 5 | return: this["args"] extends [...any, infer last] 6 | ? last extends { $outputType: infer OT } 7 | ? Array 8 | : Array 9 | : never; 10 | } 11 | 12 | export const array = (innerValidator?: Validator) => { 13 | const ctx = { 14 | chain: innerValidator ? [innerValidator] : null, 15 | } satisfies { chain: Validator[] | null }; 16 | return { 17 | name: "array" as const, 18 | $inputType: "any" as unknown as any, 19 | $outputType: ((el: string) => { 20 | const elemType = innerValidator?.$outputType ?? el; 21 | return `array<${elemType}>`; 22 | }) as unknown as ArrayType, 23 | processChain: (chain: Validator[] | null) => { 24 | if (chain !== null) { 25 | ctx.chain = chain; 26 | } 27 | return []; 28 | }, 29 | parse: (arg: unknown) => { 30 | if (!Array.isArray(arg)) throw new Error(`${arg} is not an array.`); 31 | arg.forEach((el) => { 32 | const chain = ctx.chain; 33 | if (chain !== null) { 34 | chain.at(-1)!.parse(el); 35 | } else { 36 | throw new Error( 37 | `No inner validator for array. Make sure to either call .array() after some validator or do c.array(c.someValidator())`, 38 | ); 39 | } 40 | }); 41 | return arg; 42 | }, 43 | }; 44 | }; 45 | 46 | interface NonEmpty extends Fn { 47 | return: this["arg0"] extends Array ? [T, ...T[]] : never; 48 | } 49 | 50 | export const nonEmpty = () => ({ 51 | name: "nonEmpty" as const, 52 | $inputType: "array" as unknown as Array, 53 | $outputType: ((prev: string) => prev) as unknown as NonEmpty, 54 | parse: (arg: Array) => { 55 | if (arg.length === 0) throw new Error(`Array is empty.`); 56 | return arg; 57 | }, 58 | }); 59 | 60 | interface Element extends Fn { 61 | return: this["arg0"] extends Array ? T : never; 62 | } 63 | 64 | export const element = () => { 65 | const ctx = { 66 | arrayValidator: null as Validator | null, 67 | } satisfies { arrayValidator: Validator | null }; 68 | 69 | return { 70 | nonCallable: true as const, 71 | name: "element" as const, 72 | $inputType: "array" as unknown as [any, ...any[]], 73 | processChain: (chain: Validator[] | null) => { 74 | if (chain !== null) { 75 | // trim any methods that could come after the array, like nonEmpty, min, max, etc. 76 | const copy = chain.slice(); 77 | let elem = copy.pop(); 78 | while (elem?.name !== "array") { 79 | elem = copy.pop(); 80 | } 81 | 82 | ctx.arrayValidator = elem; 83 | } 84 | return []; 85 | }, 86 | $outputType: ((prev: string) => { 87 | return prev.slice("array<".length, -1); 88 | }) as unknown as Element, 89 | parse: (arg: unknown) => { 90 | if (ctx.arrayValidator === null) { 91 | throw new Error("element must be used after an array schema"); 92 | } 93 | ctx.arrayValidator.parse([arg]); 94 | return arg; 95 | }, 96 | }; 97 | }; 98 | 99 | export const min = (min: number) => ({ 100 | name: "min" as const, 101 | $inputType: "array" as unknown as [any, ...any[]], 102 | $outputType: ((prev: string) => prev) as unknown as Identity, 103 | parse: (arg: Array) => { 104 | if (arg.length < min) throw new Error(`Array is smaller than ${min}.`); 105 | return arg; 106 | }, 107 | }); 108 | 109 | export const max = (max: number) => ({ 110 | name: "max" as const, 111 | $inputType: "array" as unknown as [any, ...any[]], 112 | $outputType: ((prev: string) => prev) as unknown as Identity, 113 | parse: (arg: Array) => { 114 | if (arg.length > max) throw new Error(`Array is greater than ${max}.`); 115 | return arg; 116 | }, 117 | }); 118 | 119 | export const length = (length: number) => ({ 120 | name: "length" as const, 121 | $inputType: "array" as unknown as [any, ...any[]], 122 | $outputType: ((prev: string) => prev) as unknown as Identity, 123 | parse: (arg: Array) => { 124 | if (arg.length !== length) 125 | throw new Error(`Array is not of length ${length}.`); 126 | return arg; 127 | }, 128 | }); 129 | -------------------------------------------------------------------------------- /src/bigint.ts: -------------------------------------------------------------------------------- 1 | export const gt = (min: bigint) => ({ 2 | name: "gt" as const, 3 | $inputType: "bigint" as unknown as bigint, 4 | $outputType: "bigint" as unknown as bigint, 5 | parse: (arg: bigint) => { 6 | if (arg <= min) 7 | throw new Error(`${arg} is smaller than or equal to ${min}.`); 8 | return arg; 9 | }, 10 | }); 11 | 12 | export const gte = (min: bigint) => ({ 13 | name: "gte" as const, 14 | $inputType: "bigint" as unknown as bigint, 15 | $outputType: "bigint" as unknown as bigint, 16 | parse: (arg: bigint) => { 17 | if (arg < min) throw new Error(`${arg} is smaller than ${min}.`); 18 | return arg; 19 | }, 20 | }); 21 | 22 | export const lt = (max: bigint) => ({ 23 | name: "lt" as const, 24 | $inputType: "bigint" as unknown as bigint, 25 | $outputType: "bigint" as unknown as bigint, 26 | parse: (arg: bigint) => { 27 | if (arg >= max) 28 | throw new Error(`${arg} is greater than or equal to ${max}.`); 29 | return arg; 30 | }, 31 | }); 32 | 33 | export const lte = (max: bigint) => ({ 34 | name: "lte" as const, 35 | $inputType: "bigint" as unknown as bigint, 36 | $outputType: "bigint" as unknown as bigint, 37 | parse: (arg: bigint) => { 38 | if (arg > max) throw new Error(`${arg} is greater than ${max}.`); 39 | return arg; 40 | }, 41 | }); 42 | 43 | export const positive = () => ({ 44 | name: "positive" as const, 45 | $inputType: "bigint" as unknown as bigint, 46 | $outputType: "bigint" as unknown as bigint, 47 | parse: (arg: bigint) => { 48 | if (arg <= 0n) throw new Error(`${arg} is not a positive bigint.`); 49 | return arg; 50 | }, 51 | }); 52 | 53 | export const nonnegative = () => ({ 54 | name: "nonnegative" as const, 55 | $inputType: "bigint" as unknown as bigint, 56 | $outputType: "bigint" as unknown as bigint, 57 | parse: (arg: bigint) => { 58 | if (arg < 0n) throw new Error(`${arg} is not a non-negative bigint.`); 59 | return arg; 60 | }, 61 | }); 62 | 63 | export const negative = () => ({ 64 | name: "negative" as const, 65 | $inputType: "bigint" as unknown as bigint, 66 | $outputType: "bigint" as unknown as bigint, 67 | parse: (arg: bigint) => { 68 | if (arg >= 0n) throw new Error(`${arg} is not a negative bigint.`); 69 | return arg; 70 | }, 71 | }); 72 | 73 | export const nonpositive = () => ({ 74 | name: "nonpositive" as const, 75 | $inputType: "bigint" as unknown as bigint, 76 | $outputType: "bigint" as unknown as bigint, 77 | parse: (arg: bigint) => { 78 | if (arg > 0n) throw new Error(`${arg} is not a non-positive bigint.`); 79 | return arg; 80 | }, 81 | }); 82 | 83 | export const multipleOf = (multiple: bigint) => ({ 84 | name: "multipleOf" as const, 85 | $inputType: "bigint" as unknown as bigint, 86 | $outputType: "bigint" as unknown as bigint, 87 | parse: (arg: bigint) => { 88 | if (arg % multiple !== 0n) 89 | throw new Error(`${arg} is not a multiple of ${multiple}.`); 90 | return arg; 91 | }, 92 | }); 93 | -------------------------------------------------------------------------------- /src/coerce.ts: -------------------------------------------------------------------------------- 1 | declare const __brand: unique symbol; 2 | type Brand = { [__brand]: B }; 3 | export type Branded = T & Brand; 4 | 5 | type Coerce = Branded<"coerce", "coerce">; 6 | 7 | export const coerce = () => ({ 8 | nonCallable: true as const, 9 | name: "coerce" as const, 10 | $inputType: "root" as const, 11 | $outputType: "coerce" as unknown as Coerce, 12 | parse: () => {}, 13 | }); 14 | 15 | export const stringCoerce = () => ({ 16 | name: "string" as const, 17 | $inputType: "coerce" as unknown as Coerce, 18 | $outputType: "string" as unknown as string, 19 | parse: (arg: unknown) => { 20 | return String(arg); 21 | }, 22 | }); 23 | 24 | export const numberCoerce = () => ({ 25 | name: "number" as const, 26 | $inputType: "coerce" as unknown as Coerce, 27 | $outputType: "number" as unknown as number, 28 | parse: (arg: unknown) => { 29 | return Number(arg); 30 | }, 31 | }); 32 | 33 | // do the same for boolean, bigint and date 34 | 35 | export const booleanCoerce = () => ({ 36 | name: "boolean" as const, 37 | $inputType: "coerce" as unknown as Coerce, 38 | $outputType: "boolean" as unknown as boolean, 39 | parse: (arg: unknown) => { 40 | return Boolean(arg); 41 | }, 42 | }); 43 | 44 | export const bigintCoerce = () => ({ 45 | name: "bigint" as const, 46 | $inputType: "coerce" as unknown as Coerce, 47 | $outputType: "bigint" as unknown as bigint, 48 | parse: (arg: any) => { 49 | return BigInt(arg); 50 | }, 51 | }); 52 | 53 | export const dateCoerce = () => ({ 54 | name: "date" as const, 55 | $inputType: "coerce" as unknown as Coerce, 56 | $outputType: "date" as unknown as Date, 57 | parse: (arg: any) => { 58 | return new Date(arg); 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /src/correttore.ts: -------------------------------------------------------------------------------- 1 | import { Apply, Fn } from "hotscript"; 2 | import { AnyFunReturning, PickByValue } from "./util.types"; 3 | import { Validator } from "./shared.types"; 4 | 5 | // union type of features that are allowed to be chained multiple times 6 | type multiChainableFeatures = "or"; 7 | type FindMultiChainableFeatures< 8 | Validators extends AnyFunReturning>[], 9 | > = keyof PickByValue< 10 | Validators, 11 | AnyFunReturning<{ name: multiChainableFeatures }> 12 | >; 13 | 14 | type GetChainableValidators< 15 | OutputType, 16 | Validators extends AnyFunReturning>[], 17 | usedFeatures = never, 18 | > = { 19 | [K in Exclude< 20 | keyof PickByValue>>, 21 | // don't show the same feature in the same chain, e.g. 22 | // `c.string().string()` should not be allowed. 23 | // However we allows some features to be multi-chainable, e.g. 24 | // `or` in `c.or(c.string()).or(c.number())` 25 | Exclude> 26 | > as K extends keyof Validators 27 | ? ReturnType["name"] 28 | : ""]: K extends keyof Validators 29 | ? ReturnType["nonCallable"] extends true 30 | ? Omit, "parse" | "$outputType"> & 31 | GetChainableValidators< 32 | ReturnType["$outputType"] extends Fn 33 | ? Apply["$outputType"], [OutputType]> 34 | : ReturnType["$outputType"], 35 | Validators, 36 | usedFeatures | K 37 | > & { 38 | parse: ( 39 | arg: unknown, 40 | ) => ReturnType["$outputType"] extends Fn 41 | ? Apply["$outputType"], [OutputType]> 42 | : ReturnType["$outputType"]; 43 | $outputType: ReturnType["$outputType"] extends Fn 44 | ? Apply["$outputType"], [OutputType]> 45 | : ReturnType["$outputType"]; 46 | } 47 | : >( 48 | ...params: Ps 49 | ) => Omit, "parse" | "$outputType"> & 50 | GetChainableValidators< 51 | ReturnType["$outputType"] extends Fn 52 | ? Apply< 53 | ReturnType["$outputType"], 54 | [OutputType, ...Ps] 55 | > 56 | : ReturnType["$outputType"], 57 | Validators, 58 | usedFeatures | K 59 | > & { 60 | // we annotate `parse` function to accept args that make sense, for example `min` will 61 | // except the arg to be string, not unknown, because we know it can only be used 62 | // in a parsers chain after `string()`. 63 | // however we need to substitute that arg with `unknown` for the library consumer, 64 | // because in their context they should be able to start the chain with any type. 65 | parse: ( 66 | arg: unknown, 67 | ) => ReturnType["$outputType"] extends Fn 68 | ? Apply< 69 | ReturnType["$outputType"], 70 | [OutputType, ...Ps] 71 | > 72 | : ReturnType["$outputType"]; 73 | // We also need to update the $outputType, so that if it's a type-level function, we call it 74 | // with the passed parameters, and if it's a type, we just return it. 75 | $outputType: ReturnType["$outputType"] extends Fn 76 | ? Apply< 77 | ReturnType["$outputType"], 78 | [OutputType, ...Ps] 79 | > 80 | : ReturnType["$outputType"]; 81 | } 82 | : never; 83 | }; 84 | 85 | const doesExtend = (A: string, B: string): boolean => { 86 | if (A === B) { 87 | return true; 88 | } 89 | 90 | if (B === "any") { 91 | return true; 92 | } 93 | 94 | if (A.startsWith("array<") && B.startsWith("array<")) { 95 | return doesExtend(A.slice(6, A.length - 1), B.slice(6, B.length - 1)); 96 | } 97 | 98 | return false; 99 | }; 100 | 101 | const callIfFun = ( 102 | maybeFn: string | AnyFunReturning, 103 | arg: any, 104 | ): string => { 105 | if (typeof maybeFn === "function") { 106 | return maybeFn(arg); 107 | } else { 108 | return maybeFn; 109 | } 110 | }; 111 | 112 | const createParserProxy = ( 113 | validators: AnyFunReturning>[], 114 | validatorsChain: Validator[], 115 | outputType: string, 116 | ): any => { 117 | return new Proxy( 118 | { $outputType: outputType }, 119 | { 120 | get(_target, key) { 121 | if (key === "parse") { 122 | return (arg: any) => { 123 | // check if `arg` passes first n - 1 constrains 124 | validatorsChain 125 | .slice(0, validatorsChain.length - 1) 126 | .forEach((p) => p.parse(arg)); 127 | 128 | // return the result of the last one 129 | return validatorsChain[validatorsChain.length - 1].parse(arg); 130 | }; 131 | } else if (key === "$outputType") { 132 | return outputType; 133 | } 134 | const applicableValidators = validators.filter((v) => { 135 | return doesExtend(outputType, v().$inputType); 136 | }); 137 | 138 | const validatorIdx = applicableValidators.findIndex( 139 | (v) => v().name === key, 140 | ); 141 | 142 | const isNonCallable = applicableValidators[validatorIdx]!().nonCallable; 143 | 144 | if (validatorIdx !== -1) { 145 | if (isNonCallable) { 146 | const validator = applicableValidators[validatorIdx](); 147 | const chain = validator.processChain 148 | ? validator.processChain(validatorsChain ?? null) 149 | : validatorsChain; 150 | 151 | return createParserProxy( 152 | validators, 153 | [...chain, validator], 154 | callIfFun( 155 | applicableValidators[validatorIdx]().$outputType, 156 | outputType, 157 | ), 158 | ); 159 | } else { 160 | return (args: any) => { 161 | const validator = applicableValidators[validatorIdx](args); 162 | const chain = validator.processChain 163 | ? validator.processChain(validatorsChain ?? null) 164 | : validatorsChain; 165 | 166 | return createParserProxy( 167 | validators, 168 | [...chain, validator], 169 | callIfFun( 170 | applicableValidators[validatorIdx](args).$outputType, 171 | outputType, 172 | ), 173 | ); 174 | }; 175 | } 176 | } else { 177 | throw new Error(`Unknown parser ${key as string}`); 178 | } 179 | }, 180 | }, 181 | ); 182 | }; 183 | 184 | export const initCorrettore = < 185 | const Validators extends AnyFunReturning>[], 186 | >( 187 | validators: Validators, 188 | ): GetChainableValidators<"root", Validators> => { 189 | return new Proxy({} as GetChainableValidators<"root", Validators>, { 190 | get(_target, key) { 191 | // the base validators (ones that can be used from `c` variable) take "unknown" as their input 192 | const applicableValidators = validators.filter((v) => 193 | doesExtend("root", v().$inputType), 194 | ); 195 | const validatorIdx = applicableValidators.findIndex( 196 | (v) => v().name === key, 197 | ); 198 | 199 | const isNonCallable = applicableValidators[validatorIdx]!().nonCallable; 200 | 201 | if (validatorIdx !== -1) { 202 | if (isNonCallable) { 203 | return createParserProxy( 204 | validators, 205 | [applicableValidators[validatorIdx]!()], 206 | applicableValidators[validatorIdx]!().$outputType, 207 | ); 208 | } else { 209 | return (args: any) => { 210 | return createParserProxy( 211 | validators, 212 | [applicableValidators[validatorIdx]!(args)], 213 | 214 | callIfFun( 215 | applicableValidators[validatorIdx](args).$outputType, 216 | "root", 217 | ), 218 | ); 219 | }; 220 | } 221 | } else { 222 | throw new Error(`Unknown parser ${key as string}`); 223 | } 224 | }, 225 | }); 226 | }; 227 | -------------------------------------------------------------------------------- /src/dates.ts: -------------------------------------------------------------------------------- 1 | export const min = (minDate: Date) => ({ 2 | name: "min" as const, 3 | $inputType: "Date" as unknown as Date, 4 | $outputType: "Date" as unknown as Date, 5 | parse: (arg: Date) => { 6 | if (arg < minDate) throw new Error(`${arg} is earlier than ${minDate}.`); 7 | return arg; 8 | }, 9 | }); 10 | 11 | export const max = (maxDate: Date) => ({ 12 | name: "max" as const, 13 | $inputType: "Date" as unknown as Date, 14 | $outputType: "Date" as unknown as Date, 15 | parse: (arg: Date) => { 16 | if (arg > maxDate) throw new Error(`${arg} is later than ${maxDate}.`); 17 | return arg; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { initCorrettore } from "./correttore"; 2 | export type { Validator, Infer } from "./shared.types"; 3 | export * from "./primitives"; 4 | export * from "./numbers"; 5 | export * as bigint from "./bigint"; 6 | export * from "./strings"; 7 | export * from "./object"; 8 | export * as arrays from "./arrays"; 9 | export * as sets from "./sets"; 10 | export * from "./other"; 11 | export * as coerce from "./coerce"; 12 | export * from "./union"; 13 | export * from "./literal"; 14 | export * as dates from "./dates"; 15 | -------------------------------------------------------------------------------- /src/literal.ts: -------------------------------------------------------------------------------- 1 | import { Fn } from "hotscript"; 2 | import { Primitive } from "./shared.types"; 3 | 4 | interface Literal extends Fn { 5 | return: this["arg1"]; 6 | } 7 | 8 | export const literal =

(lit: P) => ({ 9 | name: "literal" as const, 10 | $inputType: "root" as const, 11 | $outputType: "general" as unknown as Literal, 12 | parse: (arg: unknown) => { 13 | if (lit !== arg) { 14 | throw new Error(`${String(arg)} does not match ${String(lit)}`); 15 | } 16 | 17 | return arg; 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /src/numbers.ts: -------------------------------------------------------------------------------- 1 | export const gt = (min: number) => ({ 2 | name: "gt" as const, 3 | $inputType: "number" as unknown as number, 4 | $outputType: "number" as unknown as number, 5 | parse: (arg: number) => { 6 | if (arg <= min) 7 | throw new Error(`${arg} is smaller than or equal to ${min}.`); 8 | return arg; 9 | }, 10 | }); 11 | 12 | export const gte = (min: number) => ({ 13 | name: "gte" as const, 14 | $inputType: "number" as unknown as number, 15 | $outputType: "number" as unknown as number, 16 | parse: (arg: number) => { 17 | if (arg < min) throw new Error(`${arg} is smaller than ${min}.`); 18 | return arg; 19 | }, 20 | }); 21 | 22 | export const lt = (max: number) => ({ 23 | name: "lt" as const, 24 | $inputType: "number" as unknown as number, 25 | $outputType: "number" as unknown as number, 26 | parse: (arg: number) => { 27 | if (arg >= max) 28 | throw new Error(`${arg} is greater than or equal to ${max}.`); 29 | return arg; 30 | }, 31 | }); 32 | 33 | export const lte = (max: number) => ({ 34 | name: "lte" as const, 35 | $inputType: "number" as unknown as number, 36 | $outputType: "number" as unknown as number, 37 | parse: (arg: number) => { 38 | if (arg > max) throw new Error(`${arg} is greater than ${max}.`); 39 | return arg; 40 | }, 41 | }); 42 | 43 | export const int = () => ({ 44 | name: "int" as const, 45 | $inputType: "number" as unknown as number, 46 | $outputType: "number" as unknown as number, 47 | parse: (arg: number) => { 48 | if (!Number.isInteger(arg)) throw new Error(`${arg} is not an integer.`); 49 | return arg; 50 | }, 51 | }); 52 | 53 | export const positive = () => ({ 54 | name: "positive" as const, 55 | $inputType: "number" as unknown as number, 56 | $outputType: "number" as unknown as number, 57 | parse: (arg: number) => { 58 | if (arg <= 0) throw new Error(`${arg} is not a positive number.`); 59 | return arg; 60 | }, 61 | }); 62 | 63 | export const nonnegative = () => ({ 64 | name: "nonnegative" as const, 65 | $inputType: "number" as unknown as number, 66 | $outputType: "number" as unknown as number, 67 | parse: (arg: number) => { 68 | if (arg < 0) throw new Error(`${arg} is not a non-negative number.`); 69 | return arg; 70 | }, 71 | }); 72 | 73 | export const negative = () => ({ 74 | name: "negative" as const, 75 | $inputType: "number" as unknown as number, 76 | $outputType: "number" as unknown as number, 77 | parse: (arg: number) => { 78 | if (arg >= 0) throw new Error(`${arg} is not a negative number.`); 79 | return arg; 80 | }, 81 | }); 82 | 83 | export const nonpositive = () => ({ 84 | name: "nonpositive" as const, 85 | $inputType: "number" as unknown as number, 86 | $outputType: "number" as unknown as number, 87 | parse: (arg: number) => { 88 | if (arg > 0) throw new Error(`${arg} is not a non-positive number.`); 89 | return arg; 90 | }, 91 | }); 92 | 93 | export const multipleOf = (multiple: number) => ({ 94 | name: "multipleOf" as const, 95 | $inputType: "number" as unknown as number, 96 | $outputType: "number" as unknown as number, 97 | parse: (arg: number) => { 98 | if (arg % multiple !== 0) 99 | throw new Error(`${arg} is not a multiple of ${multiple}.`); 100 | return arg; 101 | }, 102 | }); 103 | 104 | export const finite = () => ({ 105 | name: "finite" as const, 106 | $inputType: "number" as unknown as number, 107 | $outputType: "number" as unknown as number, 108 | parse: (arg: number) => { 109 | if (!isFinite(arg)) throw new Error(`${arg} is not a finite number.`); 110 | return arg; 111 | }, 112 | }); 113 | 114 | export const safe = () => ({ 115 | name: "safe" as const, 116 | $inputType: "number" as unknown as number, 117 | $outputType: "number" as unknown as number, 118 | parse: (arg: number) => { 119 | if (arg < Number.MIN_SAFE_INTEGER || arg > Number.MAX_SAFE_INTEGER) { 120 | throw new Error(`${arg} is not a safe number.`); 121 | } 122 | return arg; 123 | }, 124 | }); 125 | -------------------------------------------------------------------------------- /src/object.ts: -------------------------------------------------------------------------------- 1 | import { Fn } from "hotscript"; 2 | import { Identity, Validator } from "./shared.types"; 3 | 4 | interface ObjectSchema extends Fn { 5 | return: MakeFieldsWithUndefinedOptional<{ 6 | [K in keyof this["arg1"]]: this["arg1"][K]["$outputType"]; 7 | }>; 8 | } 9 | 10 | export const object = < 11 | const Schema extends Record>, 12 | >( 13 | schema: Schema, 14 | ) => ({ 15 | name: "object" as const, 16 | $inputType: "root" as const, 17 | $outputType: "object" as unknown as ObjectSchema, 18 | parse: (arg: unknown) => { 19 | if (typeof arg !== "object" || arg === null || Array.isArray(arg)) { 20 | throw new Error(`${arg} is not an object.`); 21 | } 22 | 23 | const result: Record = {}; 24 | for (const k of Object.keys(schema)) { 25 | if (k in arg) { 26 | result[k] = schema[k].parse((arg as any)[k]); 27 | } else { 28 | throw new Error(`Missing property ${k}`); 29 | } 30 | } 31 | 32 | return result; 33 | }, 34 | }); 35 | 36 | type FieldsWithUndefined = Exclude< 37 | keyof T, 38 | { 39 | [K in keyof T]: T[K] extends Exclude ? K : never; 40 | }[keyof T] 41 | >; 42 | 43 | type Expand = T extends (...args: infer A) => infer R 44 | ? (...args: Expand) => Expand 45 | : T extends infer O 46 | ? { [K in keyof O]: O[K] } 47 | : never; 48 | 49 | type MakeFieldsWithUndefinedOptional = Expand< 50 | { 51 | [K in Exclude>]: T[K]; 52 | } & { [K in FieldsWithUndefined]?: T[K] } 53 | >; 54 | 55 | export const passthrough = () => { 56 | const ctx = { 57 | chain: null as Validator | null, 58 | } satisfies { chain: Validator | null }; 59 | 60 | return { 61 | name: "passthrough" as const, 62 | $inputType: "object" as unknown as any, 63 | $outputType: "object" as unknown as Identity, 64 | processChain: (chain: Validator[] | null) => { 65 | if (chain !== null) { 66 | ctx.chain = chain.at(-1)!; 67 | } 68 | return []; 69 | }, 70 | parse: (arg: Record) => { 71 | if (typeof arg !== "object" || arg === null || Array.isArray(arg)) { 72 | throw new Error(`${arg} is not an object.`); 73 | } 74 | if (ctx.chain === null) { 75 | throw new Error("passthrough must be used after an object schema"); 76 | } 77 | ctx.chain.parse(arg); 78 | 79 | const result = {}; 80 | Object.assign(result, arg); 81 | return result; 82 | }, 83 | }; 84 | }; 85 | 86 | export const strict = () => { 87 | const ctx = { 88 | chain: null as Validator | null, 89 | } satisfies { chain: Validator | null }; 90 | 91 | return { 92 | name: "strict" as const, 93 | $inputType: "object" as unknown as any, 94 | $outputType: "object" as unknown as Identity, 95 | processChain: (chain: Validator[] | null) => { 96 | if (chain !== null) { 97 | ctx.chain = chain.at(-1)!; 98 | } 99 | return []; 100 | }, 101 | parse: (arg: Record) => { 102 | if (typeof arg !== "object" || arg === null || Array.isArray(arg)) { 103 | throw new Error(`${arg} is not an object.`); 104 | } 105 | if (ctx.chain === null) { 106 | throw new Error("strict must be used after an object schema"); 107 | } 108 | 109 | const parsed = ctx.chain.parse(arg); 110 | const recognizedKeys = Object.keys(parsed); 111 | const unrecognizedKeys = Object.keys(arg).filter( 112 | (k) => !recognizedKeys.includes(k), 113 | ); 114 | if (unrecognizedKeys.length > 0) { 115 | throw new Error( 116 | `Unrecognized keys: ${unrecognizedKeys.map((k) => `'${k}'`).join(", ")}`, 117 | ); 118 | } 119 | return parsed; 120 | }, 121 | }; 122 | }; 123 | -------------------------------------------------------------------------------- /src/other.ts: -------------------------------------------------------------------------------- 1 | import { Fn } from "hotscript"; 2 | import { Validator } from "./shared.types"; 3 | 4 | interface Nullable extends Fn { 5 | return: this["args"] extends [...any, infer last] 6 | ? last extends { $outputType: infer OT } 7 | ? OT | null 8 | : last | null 9 | : never; 10 | } 11 | 12 | export const nullable = (innerValidator?: Validator) => { 13 | const ctx = { 14 | chain: innerValidator ?? null, 15 | } satisfies { chain: Validator | null }; 16 | 17 | return { 18 | name: "nullable" as const, 19 | $inputType: "any" as unknown as any, 20 | $outputType: "any" as unknown as Nullable, 21 | processChain: (chain: Validator[] | null) => { 22 | if (chain !== null) { 23 | ctx.chain = chain.at(-1)!; 24 | } 25 | return []; 26 | }, 27 | parse: (arg: any) => { 28 | if (arg === null) return arg; 29 | const chain = ctx.chain; 30 | if (chain !== null) { 31 | chain.parse(arg); 32 | } else { 33 | throw new Error( 34 | `No inner validator for array. Make sure to either call .array() after some validator or do c.array(c.someValidator())`, 35 | ); 36 | } 37 | 38 | return arg; 39 | }, 40 | }; 41 | }; 42 | 43 | interface Optional extends Fn { 44 | return: this["args"] extends [...any, infer last] 45 | ? last extends { $outputType: infer OT } 46 | ? OT | undefined 47 | : last | undefined 48 | : never; 49 | } 50 | 51 | export const optional = (innerValidator?: Validator) => { 52 | const ctx = { 53 | chain: innerValidator ?? null, 54 | } satisfies { chain: Validator | null }; 55 | 56 | return { 57 | name: "optional" as const, 58 | $inputType: "any" as unknown as any, 59 | $outputType: "any" as unknown as Optional, 60 | processChain: (chain: Validator[] | null) => { 61 | if (chain !== null) { 62 | ctx.chain = chain.at(-1)!; 63 | } 64 | return []; 65 | }, 66 | parse: (arg: any) => { 67 | if (arg === undefined) return arg; 68 | const chain = ctx.chain; 69 | if (chain !== null) { 70 | chain.parse(arg); 71 | } else { 72 | throw new Error( 73 | `No inner validator for array. Make sure to either call .array() after some validator or do c.array(c.someValidator())`, 74 | ); 75 | } 76 | 77 | return arg; 78 | }, 79 | }; 80 | }; 81 | 82 | interface Nullish extends Fn { 83 | return: this["args"] extends [...any, infer last] 84 | ? last extends { $outputType: infer OT } 85 | ? OT | null | undefined 86 | : last | null | undefined 87 | : never; 88 | } 89 | 90 | export const nullish = (innerValidator?: Validator) => { 91 | const ctx = { 92 | chain: innerValidator ?? null, 93 | } satisfies { chain: Validator | null }; 94 | 95 | return { 96 | name: "nullish" as const, 97 | $inputType: "any" as unknown as any, 98 | $outputType: "any" as unknown as Nullish, 99 | processChain: (chain: Validator[] | null) => { 100 | if (chain !== null) { 101 | ctx.chain = chain.at(-1)!; 102 | } 103 | return []; 104 | }, 105 | parse: (arg: any) => { 106 | if (arg === null || arg === undefined) return arg; 107 | const chain = ctx.chain; 108 | if (chain !== null) { 109 | chain.parse(arg); 110 | } else { 111 | throw new Error( 112 | `No inner validator for array. Make sure to either call .array() after some validator or do c.array(c.someValidator())`, 113 | ); 114 | } 115 | return arg; 116 | }, 117 | }; 118 | }; 119 | -------------------------------------------------------------------------------- /src/primitives.ts: -------------------------------------------------------------------------------- 1 | export const string = () => ({ 2 | name: "string" as const, 3 | $inputType: "root" as const, 4 | $outputType: "string" as unknown as string, 5 | parse: (arg: unknown) => { 6 | if (typeof arg !== "string") throw new Error(`${arg} is not a string.`); 7 | return arg; 8 | }, 9 | }); 10 | 11 | export const number = () => ({ 12 | name: "number" as const, 13 | $inputType: "root" as const, 14 | $outputType: "number" as unknown as number, 15 | parse: (arg: unknown) => { 16 | if (typeof arg !== "number") throw new Error(`${arg} is not a number.`); 17 | return arg; 18 | }, 19 | }); 20 | 21 | export const bigint = () => ({ 22 | name: "bigint" as const, 23 | $inputType: "root" as const, 24 | $outputType: "bigint" as unknown as bigint, 25 | parse: (arg: unknown) => { 26 | if (typeof arg !== "bigint") throw new Error(`${arg} is not a bigint.`); 27 | return arg; 28 | }, 29 | }); 30 | 31 | export const boolean = () => ({ 32 | name: "boolean" as const, 33 | $inputType: "root" as const, 34 | $outputType: "boolean" as unknown as boolean, 35 | parse: (arg: unknown) => { 36 | if (typeof arg !== "boolean") throw new Error(`${arg} is not a boolean.`); 37 | return arg; 38 | }, 39 | }); 40 | 41 | export const date = () => ({ 42 | name: "date" as const, 43 | $inputType: "root" as const, 44 | $outputType: "Date" as unknown as Date, 45 | parse: (arg: unknown) => { 46 | if (!(arg instanceof Date)) { 47 | throw new Error(`${arg} is not a Date.`); 48 | } 49 | if (arg instanceof Date && isNaN(arg.getTime())) { 50 | throw new Error(`${arg} is an invalid Date.`); 51 | } 52 | 53 | return arg; 54 | }, 55 | }); 56 | 57 | export const symbol = () => ({ 58 | name: "symbol" as const, 59 | $inputType: "root" as const, 60 | $outputType: "symbol" as unknown as symbol, 61 | parse: (arg: unknown) => { 62 | if (typeof arg !== "symbol") throw new Error(`${arg} is not a symbol.`); 63 | return arg; 64 | }, 65 | }); 66 | 67 | export const undefinedType = () => ({ 68 | name: "undefined" as const, 69 | $inputType: "root" as const, 70 | $outputType: "undefined" as unknown as undefined, 71 | parse: (arg: unknown) => { 72 | if (arg !== undefined) throw new Error(`${arg} is not undefined.`); 73 | return arg; 74 | }, 75 | }); 76 | 77 | export const nullType = () => ({ 78 | name: "null" as const, 79 | $inputType: "root" as const, 80 | $outputType: "null" as unknown as null, 81 | parse: (arg: unknown) => { 82 | if (arg !== null) throw new Error(`${arg} is not null.`); 83 | return arg; 84 | }, 85 | }); 86 | 87 | export const voidType = () => ({ 88 | name: "void" as const, 89 | $inputType: "root" as const, 90 | $outputType: "void" as unknown as void, 91 | parse: (arg: unknown) => { 92 | if (arg !== undefined) throw new Error(`${arg} is not void.`); 93 | return arg; 94 | }, 95 | }); 96 | 97 | export const anyType = () => ({ 98 | name: "any" as const, 99 | $inputType: "root" as const, 100 | $outputType: "any" as unknown as any, 101 | parse: (arg: unknown) => arg, 102 | }); 103 | 104 | export const unknownType = () => ({ 105 | name: "root" as const, 106 | $inputType: "root" as const, 107 | $outputType: "root" as const as unknown, 108 | parse: (arg: unknown) => arg, 109 | }); 110 | 111 | export const neverType = () => ({ 112 | name: "never" as const, 113 | $inputType: "root" as const, 114 | $outputType: "never" as unknown as never, 115 | parse: (arg: unknown) => { 116 | throw new Error( 117 | `Value of type ${typeof arg} cannot be assigned to type 'never'.`, 118 | ); 119 | }, 120 | }); 121 | 122 | export const nanType = () => ({ 123 | name: "nan" as const, 124 | $inputType: "root" as const, 125 | $outputType: "number" as unknown as number, 126 | parse: (arg: unknown) => { 127 | const parsedValue = parseFloat(arg as string); 128 | if (!isNaN(parsedValue)) throw new Error(`${arg} is not a nan.`); 129 | return parsedValue; 130 | }, 131 | }); 132 | -------------------------------------------------------------------------------- /src/sets.ts: -------------------------------------------------------------------------------- 1 | import { Fn } from "hotscript"; 2 | import { Identity, Validator } from "./shared.types"; 3 | 4 | interface SetType extends Fn { 5 | return: Set; 6 | } 7 | 8 | export const set = >( 9 | chain: Schema, 10 | ) => ({ 11 | name: "set" as const, 12 | $inputType: "root" as const, 13 | $outputType: "set" as unknown as SetType, 14 | parse: (arg: unknown) => { 15 | if (typeof arg !== "object" || arg === null || !(arg instanceof Set)) 16 | throw new Error(`${arg} is not a set.`); 17 | arg.forEach((el) => { 18 | if (chain !== null) { 19 | chain.parse(el); 20 | } else { 21 | throw new Error( 22 | `No inner validator for set. Make sure to do c.set(c.someValidator())`, 23 | ); 24 | } 25 | }); 26 | return arg; 27 | }, 28 | }); 29 | 30 | export const nonEmpty = () => ({ 31 | name: "nonEmpty" as const, 32 | $inputType: "set" as unknown as Set, 33 | $outputType: "set" as unknown as Identity, 34 | parse: (arg: Set) => { 35 | if (arg.size === 0) throw new Error(`set is empty.`); 36 | return arg; 37 | }, 38 | }); 39 | 40 | export const min = (min: number) => ({ 41 | name: "min" as const, 42 | $inputType: "set" as unknown as Set, 43 | $outputType: "set" as unknown as Identity, 44 | parse: (arg: Set) => { 45 | if (arg.size < min) throw new Error(`Set is smaller than ${min}.`); 46 | return arg; 47 | }, 48 | }); 49 | 50 | export const max = (max: number) => ({ 51 | name: "max" as const, 52 | $inputType: "set" as unknown as Set, 53 | $outputType: "set" as unknown as Identity, 54 | parse: (arg: Set) => { 55 | if (arg.size > max) throw new Error(`Set is greater than ${max}.`); 56 | return arg; 57 | }, 58 | }); 59 | 60 | export const size = (sizeParam: number) => ({ 61 | name: "size" as const, 62 | $inputType: "set" as unknown as Set, 63 | $outputType: "set" as unknown as Identity, 64 | parse: (arg: Set) => { 65 | if (arg.size !== sizeParam) 66 | throw new Error(`Set is not of size ${sizeParam}.`); 67 | return arg; 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /src/shared.types.ts: -------------------------------------------------------------------------------- 1 | import { Fn } from "hotscript"; 2 | 3 | export type Validator = { 4 | name: string; 5 | $inputType: Input; 6 | $outputType: Output; 7 | processChain?: (chain: Validator[] | null) => Validator[]; 8 | nonCallable?: true; 9 | parse: (arg: Input) => Output; 10 | }; 11 | 12 | export type Infer> = Schema["$outputType"]; 13 | 14 | export type Primitive = 15 | | string 16 | | number 17 | | bigint 18 | | boolean 19 | | symbol 20 | | null 21 | | undefined; 22 | 23 | export interface Identity extends Fn { 24 | return: this["arg0"]; 25 | } 26 | -------------------------------------------------------------------------------- /src/strings.ts: -------------------------------------------------------------------------------- 1 | export const max = (maxLength: number) => ({ 2 | $inputType: "string" as unknown as string, 3 | $outputType: "string" as unknown as string, 4 | name: "max" as const, 5 | parse: (arg: string) => { 6 | if (arg.length > maxLength) 7 | throw new Error(`${arg} exceeds maximum length of ${maxLength}.`); 8 | return arg; 9 | }, 10 | }); 11 | 12 | export const min = (minLength: number) => ({ 13 | $inputType: "string" as unknown as string, 14 | $outputType: "string" as unknown as string, 15 | name: "min" as const, 16 | parse: (arg: string) => { 17 | if (arg.length < minLength) 18 | throw new Error(`${arg} is shorter than minimum length of ${minLength}.`); 19 | return arg; 20 | }, 21 | }); 22 | 23 | export const length = (exactLength: number) => ({ 24 | $inputType: "string" as unknown as string, 25 | $outputType: "string" as unknown as string, 26 | name: "length" as const, 27 | parse: (arg: string) => { 28 | if (arg.length !== exactLength) 29 | throw new Error( 30 | `${arg} does not have the expected length of ${exactLength}.`, 31 | ); 32 | return arg; 33 | }, 34 | }); 35 | 36 | export const email = () => ({ 37 | $inputType: "string" as unknown as string, 38 | $outputType: "string" as unknown as string, 39 | name: "email" as const, 40 | parse: (arg: string) => { 41 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 42 | if (!emailRegex.test(arg)) throw new Error(`${arg} is not a valid email.`); 43 | return arg; 44 | }, 45 | }); 46 | 47 | export const url = () => ({ 48 | $inputType: "string" as unknown as string, 49 | $outputType: "string" as unknown as string, 50 | name: "url" as const, 51 | parse: (arg: string) => { 52 | try { 53 | new URL(arg); 54 | } catch (error) { 55 | throw new Error(`${arg} is not a valid URL.`); 56 | } 57 | return arg; 58 | }, 59 | }); 60 | 61 | export const emoji = () => ({ 62 | $inputType: "string" as unknown as string, 63 | $outputType: "string" as unknown as string, 64 | name: "emoji" as const, 65 | parse: (arg: string) => { 66 | const emojiRegex = /[\p{Emoji}]/u; 67 | if (!emojiRegex.test(arg)) 68 | throw new Error(`${arg} does not contain any emoji.`); 69 | return arg; 70 | }, 71 | }); 72 | 73 | export const uuid = () => ({ 74 | $inputType: "string" as unknown as string, 75 | $outputType: "string" as unknown as string, 76 | name: "uuid" as const, 77 | parse: (arg: string) => { 78 | const uuidRegex = /^[a-f\d]{8}-(?:[a-f\d]{4}-){3}[a-f\d]{12}$/i; 79 | if (!uuidRegex.test(arg)) throw new Error(`${arg} is not a valid UUID.`); 80 | return arg; 81 | }, 82 | }); 83 | 84 | export const cuid = () => ({ 85 | $inputType: "string" as unknown as string, 86 | $outputType: "string" as unknown as string, 87 | name: "cuid" as const, 88 | parse: (arg: string) => { 89 | const cuidRegex = /^[a-z\d]{24}$/i; 90 | if (!cuidRegex.test(arg)) throw new Error(`${arg} is not a valid cuid.`); 91 | return arg; 92 | }, 93 | }); 94 | 95 | export const cuid2 = () => ({ 96 | $inputType: "string" as unknown as string, 97 | $outputType: "string" as unknown as string, 98 | name: "cuid2" as const, 99 | parse: (arg: string) => { 100 | const cuid2Regex = /^[a-z\d]{25}$/i; 101 | if (!cuid2Regex.test(arg)) throw new Error(`${arg} is not a valid cuid2.`); 102 | return arg; 103 | }, 104 | }); 105 | 106 | export const ulid = () => ({ 107 | $inputType: "string" as unknown as string, 108 | $outputType: "string" as unknown as string, 109 | name: "ulid" as const, 110 | parse: (arg: string) => { 111 | const ulidRegex = /^[0-9A-Z]{26}$/; 112 | if (!ulidRegex.test(arg)) throw new Error(`${arg} is not a valid ulid.`); 113 | return arg; 114 | }, 115 | }); 116 | 117 | export const regex = (pattern: RegExp) => ({ 118 | $inputType: "string" as unknown as string, 119 | $outputType: "string" as unknown as string, 120 | name: "regex" as const, 121 | parse: (arg: string) => { 122 | if (!pattern.test(arg)) 123 | throw new Error(`${arg} does not match the expected pattern.`); 124 | return arg; 125 | }, 126 | }); 127 | 128 | export const includes = (substring: string) => ({ 129 | $inputType: "string" as unknown as string, 130 | $outputType: "string" as unknown as string, 131 | name: "includes" as const, 132 | parse: (arg: string) => { 133 | if (!arg.includes(substring)) 134 | throw new Error(`${arg} does not include ${substring}.`); 135 | return arg; 136 | }, 137 | }); 138 | 139 | export const startsWith = (prefix: string) => ({ 140 | $inputType: "string" as unknown as string, 141 | $outputType: "string" as unknown as string, 142 | name: "startsWith" as const, 143 | parse: (arg: string) => { 144 | if (!arg.startsWith(prefix)) 145 | throw new Error(`${arg} does not start with ${prefix}.`); 146 | return arg; 147 | }, 148 | }); 149 | 150 | export const endsWith = (suffix: string) => ({ 151 | $inputType: "string" as unknown as string, 152 | $outputType: "string" as unknown as string, 153 | name: "endsWith" as const, 154 | parse: (arg: string) => { 155 | if (!arg.endsWith(suffix)) 156 | throw new Error(`${arg} does not end with ${suffix}.`); 157 | return arg; 158 | }, 159 | }); 160 | 161 | export const datetime = () => ({ 162 | $inputType: "string" as unknown as string, 163 | $outputType: "string" as unknown as string, 164 | name: "datetime" as const, 165 | parse: (arg: string) => { 166 | const dateObject = new Date(arg); 167 | if (isNaN(dateObject.getTime())) 168 | throw new Error(`${arg} is not a valid date.`); 169 | return arg; 170 | }, 171 | }); 172 | 173 | export const ip = () => ({ 174 | $inputType: "string" as unknown as string, 175 | $outputType: "string" as unknown as string, 176 | name: "ip" as const, 177 | parse: (arg: string) => { 178 | const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/; 179 | if (!ipRegex.test(arg)) 180 | throw new Error(`${arg} is not a valid IP address.`); 181 | return arg; 182 | }, 183 | }); 184 | 185 | export const trim = () => ({ 186 | $inputType: "string" as unknown as string, 187 | $outputType: "string" as unknown as string, 188 | name: "trim" as const, 189 | parse: (arg: string) => arg.trim(), 190 | }); 191 | 192 | export const toLowerCase = () => ({ 193 | $inputType: "string" as unknown as string, 194 | $outputType: "string" as unknown as string, 195 | name: "toLowerCase" as const, 196 | parse: (arg: string) => arg.toLowerCase(), 197 | }); 198 | 199 | export const toUpperCase = () => ({ 200 | $inputType: "string" as unknown as string, 201 | $outputType: "string" as unknown as string, 202 | name: "toUpperCase" as const, 203 | parse: (arg: string) => arg.toUpperCase(), 204 | }); 205 | -------------------------------------------------------------------------------- /src/union.ts: -------------------------------------------------------------------------------- 1 | import { Fn } from "hotscript"; 2 | import { Validator } from "./shared.types"; 3 | 4 | interface OrSchema extends Fn { 5 | return: this["args"] extends [...any, infer prev, infer last] 6 | ? last extends { $outputType: infer T1 } 7 | ? prev extends { $outputType: infer T2 } 8 | ? T1 | T2 9 | : T1 | prev 10 | : prev extends { $outputType: infer T2 } 11 | ? last | T2 12 | : last | prev 13 | : never; 14 | } 15 | 16 | export const or = (innerValidator: Validator) => { 17 | const ctx = { 18 | chain: null as Validator | null, 19 | } satisfies { chain: Validator | null }; 20 | 21 | return { 22 | name: "or" as const, 23 | $inputType: "any" as unknown as T1 | T2, 24 | $outputType: "any" as unknown as OrSchema, 25 | processChain: (chain: Validator[] | null) => { 26 | if (chain !== null) { 27 | ctx.chain = chain.at(-1)!; 28 | } 29 | return []; 30 | }, 31 | parse: (arg: any) => { 32 | const chain = ctx.chain; 33 | if (chain === null) { 34 | throw new Error( 35 | `No inner validator. Make sure to do c.someValidator().or(c.otherValidator())`, 36 | ); 37 | } 38 | 39 | try { 40 | return chain.parse(arg); 41 | } catch (e) { 42 | return innerValidator.parse(arg); 43 | } 44 | }, 45 | }; 46 | }; 47 | 48 | interface Union extends Fn { 49 | return: this["arg1"][number]["$outputType"]; 50 | } 51 | 52 | export const union = []>( 53 | options: Options, 54 | ) => ({ 55 | name: "union" as const, 56 | $inputType: "root" as const, 57 | $outputType: "any" as unknown as Union, 58 | parse: (arg: unknown) => { 59 | for (const option of options) { 60 | try { 61 | return option.parse(arg); 62 | } catch (e) { 63 | // continue 64 | } 65 | } 66 | 67 | throw new Error(`No validator matched for ${arg}`); 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /src/util.types.ts: -------------------------------------------------------------------------------- 1 | export type Entries = ValueOf<{ 2 | [Key in keyof Obj]: [Key, Obj[Key]]; 3 | }>; 4 | 5 | export type ValueOf = T[keyof T]; 6 | 7 | export type FromEntries = { 8 | [Val in Entries as Val[0]]: Val[1]; 9 | }; 10 | 11 | export type PickByValue = FromEntries< 12 | Extract, [any, Condition]> 13 | >; 14 | 15 | export type OmitByValue = FromEntries< 16 | Exclude, [any, Omitted]> 17 | >; 18 | 19 | export type AnyFunReturning = (...args: any) => T; 20 | -------------------------------------------------------------------------------- /tests/arrays.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { initCorrettore, min, string, arrays, Infer, number } from "../src"; 3 | import { Equal, Expect } from "./helpers.types"; 4 | 5 | describe("arrays", () => { 6 | const c = initCorrettore([ 7 | arrays.array, 8 | number, 9 | string, 10 | min, 11 | arrays.nonEmpty, 12 | arrays.min, 13 | arrays.max, 14 | arrays.length, 15 | arrays.element, 16 | ]); 17 | 18 | test("arrays", () => { 19 | expect(() => c.array(c.string().min(2)).parse(["ab", "ba"])).not.toThrow(); 20 | expect(() => c.array(c.string()).parse([])).not.toThrow(); 21 | expect(() => c.array(c.string()).parse([42])).toThrow(); 22 | expect(() => 23 | c.array(c.string()).nonEmpty().parse(["a", "b"]), 24 | ).not.toThrow(); 25 | expect(() => c.array(c.string()).nonEmpty().parse([])).toThrow(); 26 | expect(() => 27 | c 28 | .array(c.string()) 29 | .nonEmpty() 30 | .min(3) 31 | .max(5) 32 | .length(4) 33 | .parse(["a", "b", "c", "d"]), 34 | ).not.toThrow(); 35 | expect(() => 36 | c 37 | .array(c.string()) 38 | .nonEmpty() 39 | .min(3) 40 | .max(5) 41 | .length(4) 42 | .parse(["a", "b", "d"]), 43 | ).toThrow(); 44 | 45 | const schema = c.array(c.string()); 46 | type SchemaType = Infer; 47 | type _test = Expect>; 48 | }); 49 | 50 | test("arrays alternative syntax", () => { 51 | expect(() => c.string().min(2).array().parse(["ab", "ba"])).not.toThrow(); 52 | expect(() => c.string().array().parse([])).not.toThrow(); 53 | expect(() => c.string().array().parse([42])).toThrow(); 54 | expect(() => c.string().array().nonEmpty().parse(["a", "b"])).not.toThrow(); 55 | expect(() => c.string().array().nonEmpty().parse([])).toThrow(); 56 | expect(() => 57 | c 58 | .string() 59 | .array() 60 | .nonEmpty() 61 | .min(3) 62 | .max(5) 63 | .length(4) 64 | .parse(["a", "b", "c", "d"]), 65 | ).not.toThrow(); 66 | expect(() => 67 | c 68 | .string() 69 | .array() 70 | .nonEmpty() 71 | .min(3) 72 | .max(5) 73 | .length(4) 74 | .parse(["a", "b", "d"]), 75 | ).toThrow(); 76 | 77 | const schema = c.string().array(); 78 | type SchemaType = Infer; 79 | type _test = Expect>; 80 | }); 81 | 82 | test("element", () => { 83 | const schema = c.string().min(3).array().element; 84 | type SchemaType = Infer; 85 | type _test = Expect>; 86 | expect(schema.parse("hello")).toBe("hello"); 87 | expect(() => schema.parse("ui")).toThrow(); 88 | }); 89 | 90 | test("element alt syntax", () => { 91 | const schema = c.array(c.string().min(3)).element; 92 | type SchemaType = Infer; 93 | type _test = Expect>; 94 | expect(schema.parse("hello")).toBe("hello"); 95 | expect(() => schema.parse("ui")).toThrow(); 96 | }); 97 | 98 | test("element after other array methods", () => { 99 | const schema = c.array(c.string().min(3)).nonEmpty().min(5).max(10).element; 100 | type SchemaType = Infer; 101 | type _test = Expect>; 102 | expect(schema.parse("hello")).toBe("hello"); 103 | expect(() => schema.parse("ui")).toThrow(); 104 | }); 105 | 106 | test("element after other array methods alt syntax", () => { 107 | const schema = c.string().min(3).array().nonEmpty().min(5).max(10).element; 108 | type SchemaType = Infer; 109 | type _test = Expect>; 110 | expect(schema.parse("hello")).toBe("hello"); 111 | expect(() => schema.parse("ui")).toThrow(); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /tests/coerce.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { Infer, coerce, initCorrettore } from "../src"; 3 | import { Equal, Expect } from "./helpers.types"; 4 | 5 | describe("coerce", () => { 6 | const c = initCorrettore([ 7 | coerce.coerce, 8 | coerce.stringCoerce, 9 | coerce.booleanCoerce, 10 | coerce.numberCoerce, 11 | coerce.bigintCoerce, 12 | coerce.dateCoerce, 13 | ]); 14 | 15 | test("coerce", () => { 16 | const schema = c.coerce.string(); 17 | expect(schema.parse("howdy")).toBe("howdy"); 18 | expect(schema.parse(42)).toBe("42"); 19 | 20 | type SchemaType = Infer; 21 | type _test = Expect>; 22 | }); 23 | 24 | test("autocompletion", () => { 25 | // @ts-expect-error this should't be available 26 | expect(() => c.boolean()).toThrow(); 27 | 28 | // @ts-expect-error this should't be available 29 | expect(() => c.string()).toThrow(); 30 | 31 | c.coerce.boolean(); 32 | c.coerce.string(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/dates.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { Infer, date, dates, initCorrettore } from "../src"; 3 | import { Equal, Expect } from "./helpers.types"; 4 | 5 | describe("dates", () => { 6 | const c = initCorrettore([date, dates.max, dates.min]); 7 | 8 | test("date", () => { 9 | const schema = c.date(); 10 | expect(() => schema.parse(new Date())).not.toThrow(); 11 | expect(() => schema.parse("2022-01-12T00:00:00.000Z")).toThrow(); 12 | expect(() => schema.parse(new Date("invalid"))).toThrow(); 13 | 14 | type SchemaType = Infer; 15 | type _test = Expect>; 16 | }); 17 | 18 | test("min", () => { 19 | const schema = c.date().min(new Date("2022-01-01T00:00:00.000Z")); 20 | expect(() => 21 | schema.parse(new Date("2022-01-02T00:00:00.000Z")), 22 | ).not.toThrow(); 23 | expect(() => schema.parse(new Date("2021-12-31T00:00:00.000Z"))).toThrow(); 24 | }); 25 | 26 | test("max", () => { 27 | const schema = c.date().max(new Date("2022-01-01T00:00:00.000Z")); 28 | expect(() => 29 | schema.parse(new Date("2021-12-31T00:00:00.000Z")), 30 | ).not.toThrow(); 31 | expect(() => schema.parse(new Date("2022-01-02T00:00:00.000Z"))).toThrow(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/helpers.types.ts: -------------------------------------------------------------------------------- 1 | export type Expect = T; 2 | export type Equal = 3 | (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 4 | ? true 5 | : false; 6 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { 3 | initCorrettore, 4 | string, 5 | min, 6 | number, 7 | email, 8 | Infer, 9 | object, 10 | nullable, 11 | optional, 12 | or, 13 | coerce, 14 | boolean, 15 | nullish, 16 | } from "../src"; 17 | import { Equal, Expect } from "./helpers.types"; 18 | import { 19 | array, 20 | nonEmpty, 21 | min as arrayMin, 22 | max as arrayMax, 23 | length, 24 | } from "../src/arrays"; 25 | import { literal } from "../src/literal"; 26 | import { 27 | set, 28 | nonEmpty as setNonEmpty, 29 | min as setMin, 30 | max as setMax, 31 | size as setSize, 32 | } from "../src/sets"; 33 | 34 | describe("basic tests", () => { 35 | const c = initCorrettore([ 36 | string, 37 | min, 38 | number, 39 | boolean, 40 | email, 41 | object, 42 | array, 43 | nonEmpty, 44 | arrayMin, 45 | arrayMax, 46 | length, 47 | nullable, 48 | nullish, 49 | literal, 50 | optional, 51 | or, 52 | set, 53 | setNonEmpty, 54 | setMin, 55 | setMax, 56 | setSize, 57 | coerce.coerce, 58 | coerce.stringCoerce, 59 | coerce.numberCoerce, 60 | coerce.booleanCoerce, 61 | coerce.dateCoerce, 62 | coerce.bigintCoerce, 63 | ]); 64 | 65 | // TODO: split into *.test.ts files and add more tests 66 | 67 | test("smoke tests", () => { 68 | expect(() => c.number().parse(42)).not.toThrow(); 69 | expect(() => c.number().parse("hello")).toThrow(); 70 | expect(() => c.string().parse("hello")).not.toThrow(); 71 | expect(() => c.string().parse(42)).toThrow(); 72 | expect(() => 73 | c 74 | .object({ 75 | a: c.string(), 76 | b: c.number(), 77 | }) 78 | .parse({ 79 | a: "hello", 80 | b: 42, 81 | }), 82 | ).not.toThrow(); 83 | }); 84 | 85 | test("all conditions in a chain are checked", () => { 86 | const schema = c.string().email().min(5); 87 | expect(() => schema.parse("aaa@a.pl")).not.toThrow(); 88 | expect(() => schema.parse("aaapl.com")).toThrow(); 89 | expect(() => schema.parse("a@e")).toThrow(); 90 | expect(() => schema.parse(42)).toThrow(); 91 | }); 92 | 93 | test("types", () => { 94 | const schema = c.object({ 95 | a: c.string().email().min(2), 96 | b: c.number(), 97 | c: c.object({ 98 | d: c.string(), 99 | }), 100 | }); 101 | 102 | type InferredSchema = Infer; 103 | 104 | type _typeTest = Expect< 105 | Equal< 106 | InferredSchema, 107 | { 108 | a: string; 109 | b: number; 110 | c: { 111 | d: string; 112 | }; 113 | } 114 | > 115 | >; 116 | 117 | const schema2 = c.array(c.string().email()).max(4).nonEmpty(); 118 | type InferredSchema2 = Infer; 119 | type _typeTest2 = Expect>; 120 | 121 | const schema3 = c.string().email().array().max(4).nonEmpty(); 122 | type InferredSchema3 = Infer; 123 | type _typeTest3 = Expect>; 124 | }); 125 | 126 | test("sets", () => { 127 | expect(() => 128 | c.set(c.string().min(2)).parse(new Set(["ab", "ba"])), 129 | ).not.toThrow(); 130 | expect(() => c.set(c.string()).parse(new Set())).not.toThrow(); 131 | expect(() => c.set(c.string()).parse(new Set([42]))).toThrow(); 132 | expect(() => 133 | c 134 | .set(c.string()) 135 | .nonEmpty() 136 | .parse(new Set(["a", "b"])), 137 | ).not.toThrow(); 138 | expect(() => c.set(c.string()).nonEmpty().parse(new Set([]))).toThrow(); 139 | expect(() => 140 | c 141 | .set(c.string()) 142 | .nonEmpty() 143 | .min(3) 144 | .max(5) 145 | .size(4) 146 | .parse(new Set(["a", "b", "c", "d"])), 147 | ).not.toThrow(); 148 | expect(() => 149 | c 150 | .set(c.string()) 151 | .nonEmpty() 152 | .min(3) 153 | .max(5) 154 | .size(4) 155 | .parse(new Set(["a", "b", "d"])), 156 | ).toThrow(); 157 | 158 | const schema = c.set(c.string()); 159 | type SchemaType = Infer; 160 | type _test = Expect>>; 161 | }); 162 | 163 | test("nullable", () => { 164 | const schema = c.string().nullable(); 165 | expect(() => schema.parse("hello")).not.toThrow(); 166 | expect(() => schema.parse(null)).not.toThrow(); 167 | type SchemaType = Infer; 168 | type _test = Expect>; 169 | }); 170 | 171 | test("nullable alt syntax", () => { 172 | const schema = c.nullable(c.string()); 173 | expect(() => schema.parse("hello")).not.toThrow(); 174 | expect(() => schema.parse(null)).not.toThrow(); 175 | expect(() => schema.parse(undefined)).toThrow(); 176 | expect(() => schema.parse(42)).toThrow(); 177 | 178 | type SchemaType = Infer; 179 | type _test = Expect>; 180 | }); 181 | 182 | test("optional", () => { 183 | const schema = c.string().optional(); 184 | expect(() => schema.parse("hello")).not.toThrow(); 185 | expect(() => schema.parse(undefined)).not.toThrow(); 186 | expect(() => schema.parse(null)).toThrow(); 187 | expect(() => schema.parse(42)).toThrow(); 188 | type SchemaType = Infer; 189 | type _test = Expect>; 190 | }); 191 | 192 | test("optional alt syntax", () => { 193 | const schema = c.optional(c.string()); 194 | expect(() => schema.parse("hello")).not.toThrow(); 195 | expect(() => schema.parse(undefined)).not.toThrow(); 196 | expect(() => schema.parse(null)).toThrow(); 197 | expect(() => schema.parse(42)).toThrow(); 198 | 199 | type SchemaType = Infer; 200 | type _test = Expect>; 201 | }); 202 | 203 | test("optional fields", () => { 204 | const schema = c.object({ 205 | a: c.string(), 206 | b: c.string().optional(), 207 | }); 208 | 209 | type SchemaType = Infer; 210 | type _test = Expect< 211 | Equal< 212 | SchemaType, 213 | { 214 | a: string; 215 | b?: string | undefined; 216 | } 217 | > 218 | >; 219 | }); 220 | 221 | test("nullish", () => { 222 | const schema = c.string().nullish(); 223 | expect(() => schema.parse("hello")).not.toThrow(); 224 | expect(() => schema.parse(null)).not.toThrow(); 225 | expect(() => schema.parse(undefined)).not.toThrow(); 226 | expect(() => schema.parse(42)).toThrow(); 227 | type SchemaType = Infer; 228 | type _test = Expect>; 229 | }); 230 | 231 | test("nullish alt syntax", () => { 232 | const schema = c.nullish(c.string()); 233 | expect(() => schema.parse("hello")).not.toThrow(); 234 | expect(() => schema.parse(null)).not.toThrow(); 235 | expect(() => schema.parse(undefined)).not.toThrow(); 236 | expect(() => schema.parse(42)).toThrow(); 237 | type SchemaType = Infer; 238 | type _test = Expect>; 239 | }); 240 | 241 | test("nullish fields", () => { 242 | const schema = c.object({ 243 | a: c.string(), 244 | b: c.string().nullish(), 245 | }); 246 | type SchemaType = Infer; 247 | type _test = Expect< 248 | Equal< 249 | SchemaType, 250 | { 251 | a: string; 252 | b?: string | null | undefined; 253 | } 254 | > 255 | >; 256 | }); 257 | 258 | test("literal", () => { 259 | const schema = c.literal("howdy"); 260 | expect(() => schema.parse("howdy")).not.toThrow(); 261 | expect(() => schema.parse("anything else")).toThrow(); 262 | 263 | type SchemaType = Infer; 264 | type _test = Expect>; 265 | 266 | const schema2 = c.literal(42); 267 | expect(() => schema2.parse(42)).not.toThrow(); 268 | expect(() => schema2.parse("anything else")).toThrow(); 269 | 270 | type SchemaType2 = Infer; 271 | type _test2 = Expect>; 272 | }); 273 | }); 274 | -------------------------------------------------------------------------------- /tests/objects.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { 3 | initCorrettore, 4 | string, 5 | number, 6 | Infer, 7 | object, 8 | passthrough, 9 | strict, 10 | } from "../src"; 11 | import { Equal, Expect } from "./helpers.types"; 12 | 13 | describe("objects", () => { 14 | const c = initCorrettore([object, string, number, passthrough, strict]); 15 | 16 | test("by default, object schemas strip out unrecognized keys during parsing", () => { 17 | const schema = c.object({ 18 | x: c.number(), 19 | y: c.number(), 20 | }); 21 | expect(schema.parse({ x: 1, y: 2 })).toEqual({ x: 1, y: 2 }); 22 | expect(schema.parse({ x: 1, y: 2, z: 3 })).toEqual({ x: 1, y: 2 }); 23 | expect(() => schema.parse({ x: "a", y: 2 })).toThrow(); 24 | 25 | type SchemaType = Infer; 26 | type _test = Expect>; 27 | }); 28 | 29 | test("pass through unknown keys", () => { 30 | const schema = c 31 | .object({ 32 | x: c.number(), 33 | y: c.number(), 34 | }) 35 | .passthrough(); 36 | expect(schema.parse({ x: 1, y: 2 })).toEqual({ x: 1, y: 2 }); 37 | expect(schema.parse({ x: 1, y: 2, z: 3 })).toEqual({ x: 1, y: 2, z: 3 }); 38 | expect(() => schema.parse({ x: "a", y: 2 })).toThrow(); 39 | 40 | type SchemaType = Infer; 41 | type _test = Expect>; 42 | }); 43 | 44 | test("throw if unknown keys in the input", () => { 45 | const schema = c 46 | .object({ 47 | x: c.number(), 48 | }) 49 | .strict(); 50 | expect(schema.parse({ x: 1 })).toEqual({ x: 1 }); 51 | expect(() => schema.parse({ x: 1, y: 2 })).toThrow(); 52 | expect(() => schema.parse({ x: "a" })).toThrow(); 53 | 54 | type SchemaType = Infer; 55 | type _test = Expect>; 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/union.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { 3 | Infer, 4 | boolean, 5 | initCorrettore, 6 | number, 7 | object, 8 | or, 9 | string, 10 | literal, 11 | union, 12 | url, 13 | optional, 14 | } from "../src"; 15 | import { Equal, Expect } from "./helpers.types"; 16 | 17 | const c = initCorrettore([ 18 | string, 19 | number, 20 | or, 21 | boolean, 22 | object, 23 | literal, 24 | union, 25 | url, 26 | optional, 27 | ]); 28 | 29 | describe("or", () => { 30 | test("basic", () => { 31 | const schema = c.string().or(c.number()).or(c.boolean()); 32 | expect(schema.parse("yay")).toEqual("yay"); 33 | expect(schema.parse(42)).toEqual(42); 34 | expect(() => schema.parse({ x: 42 })).toThrow(); 35 | 36 | type SchemaType = Infer; 37 | type _test = Expect>; 38 | }); 39 | 40 | test("literal", () => { 41 | const schema = c.literal("a").or(c.literal(42)).or(c.literal("b")); 42 | expect(schema.parse("a")).toEqual("a"); 43 | expect(schema.parse(42)).toEqual(42); 44 | expect(() => schema.parse("aaa")).toThrow(); 45 | 46 | type SchemaType = Infer; 47 | type _test = Expect>; 48 | }); 49 | 50 | test("object", () => { 51 | const schema = c 52 | .object({ x: number() }) 53 | .or(c.object({ y: string() })) 54 | .or(c.string()); 55 | expect(schema.parse({ x: 42 })).toEqual({ x: 42 }); 56 | expect(schema.parse({ y: "foo" })).toEqual({ y: "foo" }); 57 | expect(schema.parse("foo")).toEqual("foo"); 58 | expect(() => schema.parse({ x: "bar" })).toThrow(); 59 | expect(() => schema.parse({ y: 42 })).toThrow(); 60 | 61 | // strip keys in other schema 62 | expect(schema.parse({ x: 42, y: "foo" })).toEqual({ x: 42 }); 63 | 64 | type SchemaType = Infer; 65 | type _test = Expect< 66 | Equal 67 | >; 68 | }); 69 | 70 | test("with nulls", () => { 71 | const schema = c.string().or(c.number()).or(c.boolean()); 72 | expect(() => schema.parse(null)).toThrow(); 73 | 74 | type SchemaType = Infer; 75 | type _test = Expect>; 76 | }); 77 | }); 78 | 79 | describe("union", () => { 80 | test("basic", () => { 81 | const schema = c.union([c.string(), c.number()]); 82 | expect(schema.parse("yay")).toEqual("yay"); 83 | expect(schema.parse(42)).toEqual(42); 84 | expect(() => schema.parse({ x: 42 })).toThrow(); 85 | 86 | type SchemaType = Infer; 87 | type _test = Expect>; 88 | }); 89 | 90 | test("literals", () => { 91 | const schema = c.union([c.literal("hello"), c.number(), c.literal("hi")]); 92 | expect(schema.parse("hello")).toEqual("hello"); 93 | expect(schema.parse(42)).toEqual(42); 94 | expect(() => schema.parse({ x: 42 })).toThrow(); 95 | 96 | type SchemaType = Infer; 97 | type _test = Expect>; 98 | }); 99 | 100 | test("object", () => { 101 | const schema = c.union([ 102 | c.object({ x: number() }), 103 | c.object({ y: string() }), 104 | c.string(), 105 | ]); 106 | expect(schema.parse({ x: 42 })).toEqual({ x: 42 }); 107 | expect(schema.parse({ y: "foo" })).toEqual({ y: "foo" }); 108 | expect(schema.parse("foo")).toEqual("foo"); 109 | expect(() => schema.parse({ x: "bar" })).toThrow(); 110 | expect(() => schema.parse({ y: 42 })).toThrow(); 111 | 112 | expect(schema.parse({ x: 42, y: "foo" })).toEqual({ x: 42 }); 113 | 114 | type SchemaType = Infer; 115 | type _test = Expect< 116 | Equal 117 | >; 118 | }); 119 | 120 | test("case from zod docs", () => { 121 | const optionalUrl = c.union([c.string().url().optional(), c.literal("")]); 122 | 123 | expect(optionalUrl.parse(undefined)).toBe(undefined); 124 | expect(optionalUrl.parse("")).toBe(""); 125 | expect(optionalUrl.parse("https://zod.dev")).toBe("https://zod.dev"); 126 | expect(() => optionalUrl.parse("not a valid url")).toThrow(); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "types": ["bun-types"], 5 | "lib": ["ESNext", "DOM"], 6 | "target": "ESNext", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "allowImportingTsExtensions": true, 10 | "noEmit": true, 11 | "skipLibCheck": true 12 | }, 13 | "include": ["."] 14 | } 15 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["./src/index.ts"], 5 | clean: true, 6 | format: ["esm", "cjs"], 7 | dts: true, 8 | outDir: "./dist", 9 | minify: true, 10 | treeshake: true, 11 | }); 12 | --------------------------------------------------------------------------------