├── .npmrc ├── docs ├── src │ ├── vite-env.d.ts │ └── main.tsx ├── .gitignore ├── vite.config.ts ├── eslint.config.js ├── package.json ├── tsconfig.json ├── index.html ├── prepareResults.mjs ├── packagesPopularity.json └── results │ └── preview.svg ├── bunfig.toml ├── .prettierrc.js ├── cases ├── paseri │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ └── index.ts ├── spectypes │ ├── .babelrc │ ├── src │ │ ├── tsconfig.json │ │ └── index.ts │ └── index.ts ├── deepkit │ ├── tsconfig.json │ ├── index.ts │ ├── build │ │ ├── index.js.map │ │ ├── index.d.ts │ │ └── index.js │ └── src │ │ └── index.ts ├── ts-auto-guard │ ├── index.ts │ ├── tsconfig.json │ └── src │ │ └── index.ts ├── ts-runtime-checks │ ├── src │ │ ├── tsconfig.json │ │ └── index.ts │ └── index.ts ├── type-predicate-generator │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ ├── index.ts │ └── compile.sh ├── typia │ ├── tsconfig.json │ ├── src │ │ └── index.ts │ └── index.ts ├── arktype.ts ├── runtypes.ts ├── cleaners.ts ├── stnl.ts ├── sinclair-typebox-dynamic.ts ├── banditypes.ts ├── sinclair-typebox-ahead-of-time.ts ├── rulr.ts ├── mojotech-json-type-validation.ts ├── ts-utils.ts ├── purify-ts.ts ├── typebox │ ├── build │ │ ├── check-loose.ts │ │ └── check-strict.ts │ └── index.ts ├── sinclair-typebox-just-in-time.ts ├── suretype.ts ├── json-decoder.ts ├── tiny-schema-validator.ts ├── valita.ts ├── caketype.ts ├── ts-interface-checker.ts ├── ok-computer.ts ├── to-typed.ts ├── simple-runtypes.ts ├── computed-types.ts ├── mol_data.ts ├── ts-json-validator.ts ├── sury.ts ├── decoders.ts ├── valibot.ts ├── rescript-schema.ts ├── typeofweb-schema.ts ├── succulent.ts ├── sinclair-typemap-valibot.ts ├── vality.ts ├── io-ts.ts ├── myzod.ts ├── yup.ts ├── class-validator.ts ├── superstruct.ts ├── index.ts ├── sinclair-typemap-zod.ts ├── unknownutil.ts ├── joi.ts ├── toi.ts ├── sinclair-typebox.ts ├── aeria │ └── index.ts ├── bueno.ts ├── jointz.ts ├── sapphire-shapeshift.ts ├── mondrian-framework.ts ├── zod4.ts ├── jet-validators.ts ├── r-assign.ts ├── zod.ts ├── tson.ts ├── dhi.ts ├── effect-schema.ts ├── ajv.ts ├── parse-dont-validate.ts └── pure-parse.ts ├── .vscode └── settings.json ├── benchmarks ├── index.ts ├── helpers │ ├── types.ts │ ├── register.ts │ ├── graph.ts │ └── main.ts ├── parseStrict.ts ├── assertStrict.ts ├── assertLoose.ts └── parseSafe.ts ├── .eslintignore ├── tsconfig.json ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── pages.yml │ ├── download-packages-popularity.yml │ ├── pr.yml │ └── release.yml ├── deno.jsonc ├── renovate.json ├── .eslintrc.json ├── start.sh ├── test └── benchmarks.test.ts ├── .gitignore ├── index.ts ├── package.json ├── download-packages-popularity.ts └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | @jsr:registry=https://npm.jsr.io 3 | -------------------------------------------------------------------------------- /docs/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [install] 2 | saveTextLockfile = true 3 | 4 | [run] 5 | bun = true -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('gts/.prettierrc.json'), 3 | bracketSpacing: true, 4 | } -------------------------------------------------------------------------------- /cases/paseri/src/index.ts: -------------------------------------------------------------------------------- 1 | import { object, string, number, boolean } from '@vbudovski/paseri'; 2 | 3 | export { object, string, number, boolean }; 4 | -------------------------------------------------------------------------------- /docs/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'preact'; 2 | import { App } from './App.js'; 3 | 4 | render(, document.getElementById('root')!); 5 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build artifacts 2 | dist 3 | 4 | # copy of results accessible to the viewer app 5 | public/results 6 | public/packagesPopularity.json 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | } -------------------------------------------------------------------------------- /cases/spectypes/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-typescript"], 3 | "plugins": ["babel-plugin-spectypes"], 4 | "targets": { 5 | "node": 16 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cases/deepkit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "build" 6 | }, 7 | "include": ["src/index.ts"], 8 | "reflection": true 9 | } 10 | -------------------------------------------------------------------------------- /cases/paseri/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "Bundler", 5 | "module": "ESNext" 6 | }, 7 | "exclude": ["build"], 8 | "reflection": true 9 | } 10 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import preact from '@preact/preset-vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | // base dir for gh-pages 6 | base: 'typescript-runtime-type-benchmarks/', 7 | plugins: [preact()], 8 | }); 9 | -------------------------------------------------------------------------------- /cases/ts-auto-guard/index.ts: -------------------------------------------------------------------------------- 1 | import { isLoose } from './build/index.guard'; 2 | import { addCase } from '../../benchmarks'; 3 | 4 | addCase('ts-auto-guard', 'assertLoose', data => { 5 | if (!isLoose(data)) throw new Error('wrong type.'); 6 | return true; 7 | }); 8 | -------------------------------------------------------------------------------- /cases/spectypes/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../build", 6 | "declaration": true, 7 | "emitDeclarationOnly": true 8 | }, 9 | "include": ["index.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /cases/ts-runtime-checks/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../build", 6 | "plugins": [{ "transform": "ts-runtime-checks" }] 7 | }, 8 | "include": ["index.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /cases/deepkit/index.ts: -------------------------------------------------------------------------------- 1 | import { parseSafe, assertLoose } from './build'; 2 | import { addCase } from '../../benchmarks'; 3 | 4 | addCase('deepkit', 'parseSafe', data => { 5 | return parseSafe(data); 6 | }); 7 | addCase('deepkit', 'assertLoose', data => { 8 | return assertLoose(data); 9 | }); 10 | -------------------------------------------------------------------------------- /cases/ts-auto-guard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "build", 6 | "strict": true, 7 | "strictNullChecks": true, 8 | }, 9 | "include": ["src/index.ts", "src/index.guard.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /cases/type-predicate-generator/src/index.ts: -------------------------------------------------------------------------------- 1 | export type Loose = { 2 | number: number; 3 | negNumber: number; 4 | maxNumber: number; 5 | string: string; 6 | longString: string; 7 | boolean: boolean; 8 | deeplyNested: { 9 | foo: string; 10 | num: number; 11 | bool: boolean; 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /benchmarks/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | runAllBenchmarks, 3 | createPreviewGraph, 4 | deleteResults, 5 | } from './helpers/main'; 6 | export { 7 | addCase, 8 | type AvailableBenchmarksIds, 9 | createCase, 10 | getRegisteredBenchmarks, 11 | } from './helpers/register'; 12 | export { type UnknownData } from './helpers/types'; 13 | -------------------------------------------------------------------------------- /cases/type-predicate-generator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "module": "commonjs", 6 | "outDir": "build", 7 | "strict": true, 8 | "strictNullChecks": true 9 | }, 10 | "include": ["src/index.ts", "src/index_guards.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /cases/ts-auto-guard/src/index.ts: -------------------------------------------------------------------------------- 1 | /** @see {isLoose} ts-auto-guard:type-guard */ 2 | export interface Loose { 3 | number: number; 4 | negNumber: number; 5 | maxNumber: number; 6 | string: string; 7 | longString: string; 8 | boolean: boolean; 9 | deeplyNested: { 10 | foo: string; 11 | num: number; 12 | bool: boolean; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | docs/dist 3 | cases/spectypes/build 4 | cases/ts-runtime-checks/build 5 | cases/typia/build 6 | cases/deepkit/build 7 | cases/ts-auto-guard/build 8 | cases/ts-auto-guard/src/index.guard.ts 9 | cases/type-predicate-generator/build 10 | cases/type-predicate-generator/src/index_guards.ts 11 | cases/paseri/src/index.ts 12 | cases/paseri/build 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfigs/nodejs-module", 3 | "compilerOptions": { 4 | "lib": ["ES2021", "DOM"], 5 | "target": "ES2021", 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["**/*.ts"], 12 | "exclude": ["cases/paseri/src/*.ts", "docs"] 13 | } 14 | -------------------------------------------------------------------------------- /cases/typia/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "build", 6 | "strict": true, 7 | "plugins": [ 8 | { 9 | "transform": "typia/lib/transform", 10 | "undefined": false, 11 | } 12 | ] 13 | }, 14 | "include": ["src/index.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Provide a brief summary of the changes made, including any issues resolved. Include the purpose and context behind these updates. 4 | 5 | ## Testing 6 | 7 | Explain how the changes can be tested. 8 | 9 | ## Checklist 10 | 11 | - [ ] Conducted a self-review of the code changes. 12 | - [ ] Updated documentation, if necessary. 13 | - [ ] Added tests to validate the functionality or fix. 14 | -------------------------------------------------------------------------------- /cases/typia/src/index.ts: -------------------------------------------------------------------------------- 1 | import typia from 'typia'; 2 | 3 | interface ToBeChecked { 4 | number: number; 5 | negNumber: number; 6 | maxNumber: number; 7 | string: string; 8 | longString: string; 9 | boolean: boolean; 10 | deeplyNested: { 11 | foo: string; 12 | num: number; 13 | bool: boolean; 14 | }; 15 | } 16 | 17 | export const is = typia.createIs(); 18 | export const equals = typia.createEquals(); 19 | export const clone = typia.misc.createClone(); 20 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "unstable": [ 3 | // https://docs.deno.com/runtime/reference/cli/unstable_flags/#--unstable-sloppy-imports: needed because Deno can only read ESM modules but project lacks file extension in imports (.ts) 4 | "sloppy-imports", 5 | // https://docs.deno.com/runtime/reference/cli/unstable_flags/#--unstable-detect-cjs: needed as some of the packages are CJS but masquerade as ESM 6 | "detect-cjs" 7 | ], 8 | "compilerOptions": { 9 | "experimentalDecorators": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | ":semanticCommits" 6 | ], 7 | "labels": [ 8 | "renovatebot" 9 | ], 10 | "packageRules": [ 11 | { 12 | "updateTypes": [ 13 | "minor", 14 | "patch", 15 | "pin", 16 | "digest" 17 | ], 18 | "automerge": true 19 | }, 20 | { 21 | "depTypeList": [ 22 | "devDependencies" 23 | ], 24 | "automerge": true 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /cases/type-predicate-generator/index.ts: -------------------------------------------------------------------------------- 1 | // Importing a manually minified version because the test suite 2 | // is using ts-node that does not perform code minification 3 | // that Type Predicate Generator relies on for peak performance. 4 | // In GH codespace the numbers improved from 136.1M to 159.0M ops/s. 5 | import { isLoose } from './build/index_guards'; 6 | import { addCase } from '../../benchmarks'; 7 | 8 | addCase('type-predicate-generator', 'assertLoose', data => { 9 | if (!isLoose(data)) throw new Error('wrong type.'); 10 | return true; 11 | }); 12 | -------------------------------------------------------------------------------- /cases/arktype.ts: -------------------------------------------------------------------------------- 1 | import { type } from 'arktype'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | const t = type({ 5 | number: 'number', 6 | negNumber: 'number', 7 | maxNumber: 'number', 8 | string: 'string', 9 | longString: 'string', 10 | boolean: 'boolean', 11 | deeplyNested: { 12 | foo: 'string', 13 | num: 'number', 14 | bool: 'boolean', 15 | }, 16 | }); 17 | 18 | createCase('arktype', 'assertLoose', () => { 19 | return data => { 20 | if (t.allows(data)) return true; 21 | throw new Error('Invalid'); 22 | }; 23 | }); 24 | -------------------------------------------------------------------------------- /cases/runtypes.ts: -------------------------------------------------------------------------------- 1 | import { Boolean, Number, String, Record } from 'runtypes'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('runtypes', 'assertLoose', () => { 5 | const dataType = Record({ 6 | number: Number, 7 | negNumber: Number, 8 | maxNumber: Number, 9 | string: String, 10 | longString: String, 11 | boolean: Boolean, 12 | deeplyNested: Record({ 13 | foo: String, 14 | num: Number, 15 | bool: Boolean, 16 | }), 17 | }); 18 | 19 | return data => { 20 | dataType.check(data); 21 | 22 | return true; 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /cases/cleaners.ts: -------------------------------------------------------------------------------- 1 | import { asBoolean, asNumber, asObject, asString } from 'cleaners'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | const asT = asObject({ 5 | number: asNumber, 6 | negNumber: asNumber, 7 | maxNumber: asNumber, 8 | string: asString, 9 | longString: asString, 10 | boolean: asBoolean, 11 | deeplyNested: asObject({ 12 | foo: asString, 13 | num: asNumber, 14 | bool: asBoolean, 15 | }), 16 | }); 17 | 18 | createCase('cleaners', 'assertLoose', () => { 19 | return data => { 20 | if (asT(data)) return true; 21 | throw new Error('Invalid'); 22 | }; 23 | }); 24 | -------------------------------------------------------------------------------- /docs/eslint.config.js: -------------------------------------------------------------------------------- 1 | // generated with `npm init @eslint/config@latest` 2 | import js from '@eslint/js'; 3 | import globals from 'globals'; 4 | import tseslint from 'typescript-eslint'; 5 | import { defineConfig, globalIgnores } from 'eslint/config'; 6 | 7 | export default defineConfig([ 8 | globalIgnores(['dist', 'eslint.config.js']), 9 | { 10 | files: ['src/**/*.{ts, tsx}'], 11 | plugins: { js }, 12 | extends: ['js/recommended'], 13 | }, 14 | { 15 | files: ['src/**/*.{ts, tsx}'], 16 | languageOptions: { globals: globals.browser }, 17 | }, 18 | tseslint.configs.recommended, 19 | ]); 20 | -------------------------------------------------------------------------------- /cases/stnl.ts: -------------------------------------------------------------------------------- 1 | import { build, t } from 'stnl'; 2 | 3 | import { createCase } from '../benchmarks'; 4 | 5 | const assertLoose = t.dict({ 6 | number: t.float, 7 | negNumber: t.float, 8 | maxNumber: t.float, 9 | string: t.string, 10 | longString: t.string, 11 | boolean: t.bool, 12 | deeplyNested: t.dict({ 13 | foo: t.string, 14 | num: t.float, 15 | bool: t.bool, 16 | }), 17 | }); 18 | 19 | createCase('stnl (just-in-time)', 'assertLoose', () => { 20 | const check = build.json.assert.compile(assertLoose); 21 | 22 | return data => { 23 | if (check(data)) return true; 24 | throw null; 25 | }; 26 | }); 27 | -------------------------------------------------------------------------------- /cases/typia/index.ts: -------------------------------------------------------------------------------- 1 | import { is, equals, clone } from './build'; 2 | import { addCase } from '../../benchmarks'; 3 | 4 | addCase('typia', 'parseSafe', data => { 5 | if (!is(data)) throw new Error('wrong type.'); 6 | return clone(data); 7 | }); 8 | addCase('typia', 'parseStrict', data => { 9 | if (!equals(data)) throw new Error('wrong type.'); 10 | return data; 11 | }); 12 | addCase('typia', 'assertStrict', data => { 13 | if (!equals(data)) throw new Error('wrong type.'); 14 | return true; 15 | }); 16 | addCase('typia', 'assertLoose', data => { 17 | if (!is(data)) throw new Error('wrong type.'); 18 | return true; 19 | }); 20 | -------------------------------------------------------------------------------- /cases/sinclair-typebox-dynamic.ts: -------------------------------------------------------------------------------- 1 | import { createCase } from '../benchmarks'; 2 | import { Value } from '@sinclair/typebox/value'; 3 | import { Loose, Strict } from './sinclair-typebox'; 4 | 5 | createCase('@sinclair/typebox-(dynamic)', 'assertLoose', () => { 6 | return data => { 7 | if (!Value.Check(Loose, data)) { 8 | throw new Error('validation failure'); 9 | } 10 | return true; 11 | }; 12 | }); 13 | createCase('@sinclair/typebox-(dynamic)', 'assertStrict', () => { 14 | return data => { 15 | if (!Value.Check(Strict, data)) { 16 | throw new Error('validation failure'); 17 | } 18 | return true; 19 | }; 20 | }); 21 | -------------------------------------------------------------------------------- /cases/banditypes.ts: -------------------------------------------------------------------------------- 1 | import { boolean, number, object, string } from 'banditypes'; 2 | import { addCase } from '../benchmarks'; 3 | 4 | const dataTypeSafe = object({ 5 | number: number(), 6 | negNumber: number(), 7 | maxNumber: number(), 8 | string: string(), 9 | longString: string(), 10 | boolean: boolean(), 11 | deeplyNested: object({ 12 | foo: string(), 13 | num: number(), 14 | bool: boolean(), 15 | }), 16 | }); 17 | 18 | addCase('banditypes', 'parseSafe', data => { 19 | return dataTypeSafe(data); 20 | }); 21 | 22 | addCase('banditypes', 'assertLoose', data => { 23 | dataTypeSafe(data); 24 | 25 | return true; 26 | }); 27 | -------------------------------------------------------------------------------- /cases/sinclair-typebox-ahead-of-time.ts: -------------------------------------------------------------------------------- 1 | import { createCase } from '../benchmarks'; 2 | import { CheckLoose } from './typebox/build/check-loose'; 3 | import { CheckStrict } from './typebox/build/check-strict'; 4 | 5 | createCase('@sinclair/typebox-(ahead-of-time)', 'assertLoose', () => { 6 | return data => { 7 | if (!CheckLoose(data)) { 8 | throw new Error('validation failure'); 9 | } 10 | return true; 11 | }; 12 | }); 13 | createCase('@sinclair/typebox-(ahead-of-time)', 'assertStrict', () => { 14 | return data => { 15 | if (!CheckStrict(data)) { 16 | throw new Error('validation failure'); 17 | } 18 | return true; 19 | }; 20 | }); 21 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/", 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": "latest", 6 | "project": "./tsconfig.json", 7 | "sourceType": "module" 8 | }, 9 | "ignorePatterns": ["docs/"], 10 | "rules": { 11 | "node/no-unpublished-import": "off", 12 | "@typescript-eslint/consistent-type-imports": [ 13 | "error", 14 | { 15 | "prefer": "type-imports", 16 | "fixStyle": "separate-type-imports" 17 | } 18 | ], 19 | "@typescript-eslint/consistent-type-exports": "error", 20 | "@typescript-eslint/no-import-type-side-effects": "error" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cases/ts-runtime-checks/index.ts: -------------------------------------------------------------------------------- 1 | import { assertStrict, assertLoose, parseStrict } from './build'; 2 | import { addCase } from '../../benchmarks'; 3 | 4 | addCase('ts-runtime-checks', 'parseStrict', data => { 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | return parseStrict(data as any); 7 | }); 8 | 9 | addCase('ts-runtime-checks', 'assertStrict', data => { 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | return assertStrict(data as any); 12 | }); 13 | 14 | addCase('ts-runtime-checks', 'assertLoose', data => { 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | return assertLoose(data as any); 17 | }); 18 | -------------------------------------------------------------------------------- /cases/rulr.ts: -------------------------------------------------------------------------------- 1 | import { object, number, string, boolean } from 'rulr'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('rulr', 'parseSafe', () => { 5 | const dataType = object({ 6 | bail: true, 7 | required: { 8 | number, 9 | negNumber: number, 10 | maxNumber: number, 11 | string, 12 | longString: string, 13 | boolean: boolean, 14 | deeplyNested: object({ 15 | bail: true, 16 | required: { 17 | foo: string, 18 | num: number, 19 | bool: boolean, 20 | }, 21 | }), 22 | }, 23 | }); 24 | 25 | return data => { 26 | return dataType(data); 27 | }; 28 | }); 29 | -------------------------------------------------------------------------------- /cases/type-predicate-generator/compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux -o pipefail 4 | 5 | # jump into the current script directory 6 | cd "$(dirname "$0")" 7 | 8 | # remove the old predicate file if exists 9 | rimraf src/index_guards.ts 10 | # generate a new predicate file (src/index_guards.ts) 11 | type-predicate-generator src/index.ts 12 | 13 | # remove the old build files if exists 14 | rimraf build/ 15 | # type check and compile to JS 16 | tsc -p tsconfig.json 17 | # minify the resulting compiled source 18 | esbuild --loader=js --minify < build/index_guards.js > build/index_guards.min.js 19 | rimraf build/index_guards.js 20 | mv build/index_guards.min.js build/index_guards.js 21 | -------------------------------------------------------------------------------- /cases/deepkit/build/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AA6BA,kCAGC;AAOD,oCAEC;AAKD,kCAEC;AASD,8BAEC;AA3DD,wCAAmE;;AAgBnE,MAAM,aAAa,IAAG,2BAAoB,qCAApB,IAAA,2BAAoB,GAAe,CAAA,CAAC;AAC1D,MAAM,eAAe,IAAG,mBAAY,qCAAZ,IAAA,mBAAY,GAAe,CAAA,CAAC;AAEpD;;;;;;;;;GASG;AACH,SAAgB,WAAW,CAAC,KAAc;IACxC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAY;QAAE,MAAM,IAAI,KAAK,CAAC,aAAa,CAAC,CAAC;IACrE,OAAO,IAAI,CAAC;AACd,CAAC;;AAED;;;;GAIG;AACH,SAAgB,YAAY;IAC1B,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;AACpC,CAAC;;AAED;;GAEG;AACH,SAAgB,WAAW;IACzB,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;AACpC,CAAC;;AAED;;;;;;GAMG;AACH,SAAgB,SAAS,CAAC,KAAc;IACtC,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC"} -------------------------------------------------------------------------------- /cases/mojotech-json-type-validation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | string, 3 | number, 4 | object, 5 | boolean, 6 | } from '@mojotech/json-type-validation'; 7 | import { createCase } from '../benchmarks'; 8 | 9 | createCase('@mojotech/json-type-validation', 'parseSafe', () => { 10 | const dataType = object({ 11 | number: number(), 12 | negNumber: number(), 13 | maxNumber: number(), 14 | string: string(), 15 | longString: string(), 16 | boolean: boolean(), 17 | deeplyNested: object({ 18 | foo: string(), 19 | num: number(), 20 | bool: boolean(), 21 | }), 22 | }); 23 | 24 | return data => { 25 | return dataType.runWithException(data); 26 | }; 27 | }); 28 | -------------------------------------------------------------------------------- /cases/ts-utils.ts: -------------------------------------------------------------------------------- 1 | import { object, number, boolean, string } from '@ailabs/ts-utils/dist/decoder'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('ts-utils', 'parseSafe', () => { 5 | const dataType = object('Data', { 6 | number, 7 | negNumber: number, 8 | maxNumber: number, 9 | string, 10 | longString: string, 11 | boolean, 12 | deeplyNested: object('DeeplyNested', { 13 | foo: string, 14 | num: number, 15 | bool: boolean, 16 | }), 17 | }); 18 | 19 | return data => { 20 | const res = dataType(data); 21 | 22 | if (res.error()) { 23 | throw res.error(); 24 | } 25 | 26 | return res.toMaybe().value(); 27 | }; 28 | }); 29 | -------------------------------------------------------------------------------- /cases/purify-ts.ts: -------------------------------------------------------------------------------- 1 | import { Codec, string, number, boolean } from 'purify-ts'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('purify-ts', 'parseSafe', () => { 5 | const dataType = Codec.interface({ 6 | number, 7 | negNumber: number, 8 | maxNumber: number, 9 | string, 10 | longString: string, 11 | boolean, 12 | deeplyNested: Codec.interface({ 13 | foo: string, 14 | num: number, 15 | bool: boolean, 16 | }), 17 | }); 18 | 19 | return data => { 20 | const decodedData = dataType.decode(data); 21 | 22 | if (decodedData.isRight()) { 23 | return decodedData.extract(); 24 | } 25 | 26 | throw new Error('Invalid'); 27 | }; 28 | }); 29 | -------------------------------------------------------------------------------- /cases/typebox/build/check-loose.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ export const CheckLoose = (() => {return function check(value: any): boolean { 2 | return ( 3 | (typeof value === 'object' && value !== null) && 4 | typeof value.number === 'number' && 5 | typeof value.negNumber === 'number' && 6 | typeof value.maxNumber === 'number' && 7 | (typeof value.string === 'string') && 8 | (typeof value.longString === 'string') && 9 | (typeof value.boolean === 'boolean') && 10 | (typeof value.deeplyNested === 'object' && value.deeplyNested !== null) && 11 | (typeof value.deeplyNested.foo === 'string') && 12 | typeof value.deeplyNested.num === 'number' && 13 | (typeof value.deeplyNested.bool === 'boolean') 14 | ) 15 | }})(); -------------------------------------------------------------------------------- /cases/ts-runtime-checks/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Assert, ExactProps } from 'ts-runtime-checks'; 2 | 3 | interface ToBeChecked { 4 | number: number; 5 | negNumber: number; 6 | maxNumber: number; 7 | string: string; 8 | longString: string; 9 | boolean: boolean; 10 | deeplyNested: { 11 | foo: string; 12 | num: number; 13 | bool: boolean; 14 | }; 15 | } 16 | 17 | export const parseStrict = (value: Assert>) => value; 18 | 19 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 20 | export const assertStrict = (_value: Assert>) => true; 21 | 22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 | export const assertLoose = (_value: Assert) => true; 24 | -------------------------------------------------------------------------------- /cases/sinclair-typebox-just-in-time.ts: -------------------------------------------------------------------------------- 1 | import { createCase } from '../benchmarks'; 2 | import { TypeCompiler } from '@sinclair/typebox/compiler'; 3 | import { Loose, Strict } from './sinclair-typebox'; 4 | 5 | const CheckLoose = TypeCompiler.Compile(Loose); 6 | const CheckStrict = TypeCompiler.Compile(Strict); 7 | 8 | createCase('@sinclair/typebox-(just-in-time)', 'assertLoose', () => { 9 | return data => { 10 | if (!CheckLoose.Check(data)) { 11 | throw new Error('validation failure'); 12 | } 13 | return true; 14 | }; 15 | }); 16 | createCase('@sinclair/typebox-(just-in-time)', 'assertStrict', () => { 17 | return data => { 18 | if (!CheckStrict.Check(data)) { 19 | throw new Error('validation failure'); 20 | } 21 | return true; 22 | }; 23 | }); 24 | -------------------------------------------------------------------------------- /cases/suretype.ts: -------------------------------------------------------------------------------- 1 | import { v, compile } from 'suretype'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('suretype', 'assertLoose', () => { 5 | const dataSchema = v.object({ 6 | number: v.number().required(), 7 | negNumber: v.number().required(), 8 | maxNumber: v.number().required(), 9 | string: v.string().required(), 10 | longString: v.string().required(), 11 | boolean: v.boolean().required(), 12 | deeplyNested: v 13 | .object({ 14 | foo: v.string().required(), 15 | num: v.number().required(), 16 | bool: v.boolean().required(), 17 | }) 18 | .required(), 19 | }); 20 | 21 | const ensureData = compile(dataSchema, { ensure: true }); 22 | 23 | return data => { 24 | ensureData(data); 25 | 26 | return true; 27 | }; 28 | }); 29 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-runtime-type-benchmarks-ui", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "prepare-results": "node prepareResults.mjs", 8 | "lint": "eslint", 9 | "dev": "npm run prepare-results && vite", 10 | "build": "npm run prepare-results && tsc -b && vite build", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "preact": "^10.26.5", 15 | "vega": "^5.30.0", 16 | "vega-lite": "^5.21.0" 17 | }, 18 | "devDependencies": { 19 | "@eslint/js": "^9.25.1", 20 | "@preact/preset-vite": "^2.9.3", 21 | "eslint": "^9.25.1", 22 | "globals": "^16.0.0", 23 | "prettier": "^3.3.3", 24 | "typescript": "^5.8.3", 25 | "typescript-eslint": "^8.31.0", 26 | "vite": "^7.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cases/json-decoder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | objectDecoder, 3 | stringDecoder, 4 | numberDecoder, 5 | boolDecoder, 6 | } from 'json-decoder'; 7 | import { createCase } from '../benchmarks'; 8 | 9 | createCase('json-decoder', 'parseSafe', () => { 10 | const dataType = objectDecoder({ 11 | number: numberDecoder, 12 | negNumber: numberDecoder, 13 | maxNumber: numberDecoder, 14 | string: stringDecoder, 15 | longString: stringDecoder, 16 | boolean: boolDecoder, 17 | deeplyNested: objectDecoder({ 18 | foo: stringDecoder, 19 | num: numberDecoder, 20 | bool: boolDecoder, 21 | }), 22 | }); 23 | 24 | return data => { 25 | const res = dataType.decode(data); 26 | 27 | if (res.type === 'ERR') { 28 | throw new Error(res.message); 29 | } 30 | 31 | return res.value; 32 | }; 33 | }); 34 | -------------------------------------------------------------------------------- /cases/typebox/build/check-strict.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ export const CheckStrict = (() => {return function check(value: any): boolean { 2 | return ( 3 | (typeof value === 'object' && value !== null) && 4 | typeof value.number === 'number' && 5 | typeof value.negNumber === 'number' && 6 | typeof value.maxNumber === 'number' && 7 | (typeof value.string === 'string') && 8 | (typeof value.longString === 'string') && 9 | (typeof value.boolean === 'boolean') && 10 | (typeof value.deeplyNested === 'object' && value.deeplyNested !== null) && 11 | (typeof value.deeplyNested.foo === 'string') && 12 | typeof value.deeplyNested.num === 'number' && 13 | (typeof value.deeplyNested.bool === 'boolean') && 14 | Object.getOwnPropertyNames(value.deeplyNested).length === 3 && 15 | Object.getOwnPropertyNames(value).length === 7 16 | ) 17 | }})(); -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | /* Preact Config */ 23 | "jsx": "react-jsx", 24 | "jsxImportSource": "preact", 25 | "paths": { 26 | "react": ["./node_modules/preact/compat/"], 27 | "react-dom": ["./node_modules/preact/compat/"] 28 | } 29 | }, 30 | "include": ["src"] 31 | } 32 | -------------------------------------------------------------------------------- /cases/tiny-schema-validator.ts: -------------------------------------------------------------------------------- 1 | import { createSchema, _ } from 'tiny-schema-validator'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | // Define the Strict schema with additional property constraints 5 | export const Strict = createSchema({ 6 | number: _.number(), 7 | negNumber: _.number(), 8 | maxNumber: _.number(), 9 | string: _.string(), 10 | longString: _.string(), 11 | boolean: _.boolean(), 12 | deeplyNested: _.record({ 13 | foo: _.string(), 14 | num: _.number(), 15 | bool: _.boolean(), 16 | }), 17 | }); 18 | 19 | createCase('tiny-schema-validator', 'assertStrict', () => { 20 | return data => { 21 | if (!Strict.is(data)) { 22 | throw new Error('validation failure'); 23 | } 24 | return true; 25 | }; 26 | }); 27 | 28 | createCase('tiny-schema-validator', 'parseStrict', () => { 29 | return data => { 30 | return Strict.produce(data); 31 | }; 32 | }); 33 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | Runtype Benchmarks 12 | 16 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /cases/valita.ts: -------------------------------------------------------------------------------- 1 | import * as v from '@badrap/valita'; 2 | import { addCase } from '../benchmarks'; 3 | 4 | const dataType = v.object({ 5 | number: v.number(), 6 | negNumber: v.number(), 7 | maxNumber: v.number(), 8 | string: v.string(), 9 | longString: v.string(), 10 | boolean: v.boolean(), 11 | deeplyNested: v.object({ 12 | foo: v.string(), 13 | num: v.number(), 14 | bool: v.boolean(), 15 | }), 16 | }); 17 | 18 | addCase('valita', 'parseSafe', data => { 19 | return dataType.parse(data, { mode: 'strip' }); 20 | }); 21 | 22 | addCase('valita', 'parseStrict', data => { 23 | return dataType.parse(data, { mode: 'strict' }); 24 | }); 25 | 26 | addCase('valita', 'assertLoose', data => { 27 | dataType.parse(data, { mode: 'passthrough' }); 28 | 29 | return true; 30 | }); 31 | 32 | addCase('valita', 'assertStrict', data => { 33 | dataType.parse(data, { mode: 'strict' }); 34 | 35 | return true; 36 | }); 37 | -------------------------------------------------------------------------------- /cases/caketype.ts: -------------------------------------------------------------------------------- 1 | import { bake, boolean, number, string } from 'caketype'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | const cake = bake({ 5 | number: number, 6 | negNumber: number, 7 | maxNumber: number, 8 | string: string, 9 | longString: string, 10 | boolean: boolean, 11 | deeplyNested: { 12 | foo: string, 13 | num: number, 14 | bool: boolean, 15 | }, 16 | }); 17 | 18 | // Safe parsing is not supported because extra keys are not removed from the input. 19 | 20 | createCase('caketype', 'parseStrict', () => data => { 21 | if (cake.is(data)) { 22 | return data; 23 | } 24 | throw new Error(); 25 | }); 26 | 27 | createCase('caketype', 'assertLoose', () => data => { 28 | if (cake.isShape(data)) { 29 | return true; 30 | } 31 | throw new Error(); 32 | }); 33 | 34 | createCase('caketype', 'assertStrict', () => data => { 35 | if (cake.is(data)) { 36 | return true; 37 | } 38 | throw new Error(); 39 | }); 40 | -------------------------------------------------------------------------------- /cases/typebox/index.ts: -------------------------------------------------------------------------------- 1 | import { TypeCompiler } from '@sinclair/typebox/compiler'; 2 | import type { TSchema } from '@sinclair/typebox'; 3 | import { Loose, Strict } from '../sinclair-typebox'; 4 | import { writeFileSync } from 'node:fs'; 5 | 6 | // typebox assertion routines require a named shim before writing as modules 7 | function CompileFunction(name: string, schema: T): string { 8 | return `/* eslint-disable */ export const ${name} = (() => {${TypeCompiler.Code( 9 | schema, 10 | { language: 'typescript' }, 11 | )}})();`; 12 | } 13 | 14 | // compiles the functions as string 15 | const CheckLoose = CompileFunction('CheckLoose', Loose); 16 | const CheckStrict = CompileFunction('CheckStrict', Strict); 17 | 18 | // writes to disk. target directory read from argv, see npm script 'compile:typebox' for configuration 19 | const target = process.argv[2]; 20 | writeFileSync(`${target}/check-loose.ts`, CheckLoose); 21 | writeFileSync(`${target}/check-strict.ts`, CheckStrict); 22 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | ENV_TYPE=$1 6 | 7 | if [ "$ENV_TYPE" = "NODE" ]; then 8 | RUNTIME_SCRIPT="npm" 9 | RUNTIME="node" 10 | RUNTIME_VERSION="${NODE_VERSION:-$(node -v)}" 11 | elif [ "$ENV_TYPE" = "BUN" ]; then 12 | RUNTIME_SCRIPT="bun" 13 | RUNTIME="bun" 14 | RUNTIME_VERSION="${BUN_VERSION:-$(bun -v)}" 15 | elif [ "$ENV_TYPE" = "DENO" ]; then 16 | RUNTIME_SCRIPT="deno" 17 | RUNTIME="deno" 18 | RUNTIME_VERSION="${DENO_VERSION:-$(deno -v | awk '{ print $2 }')}" 19 | else 20 | echo "Unsupported environment: $ENV_TYPE" 21 | exit 1 22 | fi 23 | 24 | export RUNTIME 25 | export RUNTIME_VERSION 26 | 27 | if [ "$ENV_TYPE" = "NODE" ]; then 28 | $RUNTIME_SCRIPT run start 29 | elif [ "$ENV_TYPE" = "BUN" ]; then 30 | $RUNTIME_SCRIPT run start:bun 31 | elif [ "$ENV_TYPE" = "DENO" ]; then 32 | $RUNTIME_SCRIPT task start:deno 33 | else 34 | echo "Unsupported environment: $ENV_TYPE" 35 | exit 1 36 | fi 37 | 38 | if [ "$ENV_TYPE" = "NODE" ]; then 39 | $RUNTIME_SCRIPT run start create-preview-svg 40 | fi 41 | -------------------------------------------------------------------------------- /cases/ts-interface-checker.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'ts-interface-checker'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('ts-interface-checker', 'assertLoose', () => { 5 | const dataType = t.iface([], { 6 | number: 'number', 7 | negNumber: 'number', 8 | maxNumber: 'number', 9 | string: 'string', 10 | longString: 'string', 11 | boolean: 'boolean', 12 | deeplyNested: t.iface([], { 13 | foo: 'string', 14 | num: 'number', 15 | bool: 'boolean', 16 | }), 17 | }); 18 | 19 | const suite = { dataType }; 20 | 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | const dataTypeChecker = t.createCheckers(suite).dataType as t.CheckerT; 23 | 24 | return data => { 25 | if (dataTypeChecker.test(data)) { 26 | return true; 27 | } 28 | 29 | // Calling .check() provides a more helpful error, but does not (at the moment) include a 30 | // typescript type guard like .test() above. 31 | dataTypeChecker.check(data); 32 | 33 | throw new Error('Invalid'); 34 | }; 35 | }); 36 | -------------------------------------------------------------------------------- /cases/spectypes/index.ts: -------------------------------------------------------------------------------- 1 | import { parseSafe, parseStrict, assertLoose } from './build'; 2 | import { addCase } from '../../benchmarks'; 3 | 4 | addCase('spectypes', 'parseSafe', data => { 5 | const parsed = parseSafe(data); 6 | 7 | if (parsed.tag === 'failure') { 8 | throw new Error(JSON.stringify(parsed.failure)); 9 | } 10 | 11 | return parsed.success; 12 | }); 13 | 14 | addCase('spectypes', 'parseStrict', data => { 15 | const parsed = parseStrict(data); 16 | 17 | if (parsed.tag === 'failure') { 18 | throw new Error(JSON.stringify(parsed.failure)); 19 | } 20 | 21 | return parsed.success; 22 | }); 23 | 24 | addCase('spectypes', 'assertLoose', data => { 25 | const parsed = assertLoose(data); 26 | 27 | if (parsed.tag === 'failure') { 28 | throw new Error(JSON.stringify(parsed.failure)); 29 | } 30 | 31 | return true; 32 | }); 33 | 34 | addCase('spectypes', 'assertStrict', data => { 35 | const parsed = parseStrict(data); 36 | 37 | if (parsed.tag === 'failure') { 38 | throw new Error(JSON.stringify(parsed.failure)); 39 | } 40 | 41 | return true; 42 | }); 43 | -------------------------------------------------------------------------------- /cases/ok-computer.ts: -------------------------------------------------------------------------------- 1 | import { boolean, number, object, string, assert } from 'ok-computer'; 2 | import { type UnknownData, addCase } from '../benchmarks'; 3 | 4 | const dataType = object({ 5 | number: number, 6 | negNumber: number, 7 | maxNumber: number, 8 | string: string, 9 | longString: string, 10 | boolean: boolean, 11 | deeplyNested: object({ 12 | foo: string, 13 | num: number, 14 | bool: boolean, 15 | }), 16 | }); 17 | 18 | const dataTypeLoose = object( 19 | { 20 | number: number, 21 | negNumber: number, 22 | maxNumber: number, 23 | string: string, 24 | longString: string, 25 | boolean: boolean, 26 | deeplyNested: object( 27 | { 28 | foo: string, 29 | num: number, 30 | bool: boolean, 31 | }, 32 | { allowUnknown: true }, 33 | ), 34 | }, 35 | { allowUnknown: true }, 36 | ); 37 | 38 | addCase('ok-computer', 'assertStrict', (data: UnknownData) => { 39 | assert(dataType(data)); 40 | return true; 41 | }); 42 | 43 | addCase('ok-computer', 'assertLoose', (data: UnknownData) => { 44 | assert(dataTypeLoose(data)); 45 | return true; 46 | }); 47 | -------------------------------------------------------------------------------- /cases/to-typed.ts: -------------------------------------------------------------------------------- 1 | import { addCase } from '../benchmarks'; 2 | import { Guard, Cast } from 'to-typed'; 3 | 4 | const model = { 5 | number: 0, 6 | negNumber: 0, 7 | maxNumber: 0, 8 | string: '', 9 | longString: '', 10 | boolean: false, 11 | deeplyNested: { 12 | foo: '', 13 | num: 0, 14 | bool: false, 15 | }, 16 | }; 17 | 18 | const guardLoose = Guard.is(model); 19 | const guardStrict = guardLoose.config({ keyGuarding: 'strict' }); 20 | const castLoose = Cast.as(model); 21 | 22 | addCase('to-typed', 'assertLoose', data => { 23 | if (guardLoose.guard(data)) { 24 | return true; 25 | } 26 | 27 | throw new Error('Invalid'); 28 | }); 29 | 30 | addCase('to-typed', 'assertStrict', data => { 31 | if (guardStrict.guard(data)) { 32 | return true; 33 | } 34 | 35 | throw new Error('Invalid'); 36 | }); 37 | 38 | addCase('to-typed', 'parseSafe', data => { 39 | return castLoose.cast(data).else(() => { 40 | throw new Error('Invalid'); 41 | }); 42 | }); 43 | 44 | addCase('to-typed', 'parseStrict', data => { 45 | return guardStrict.cast(data).else(() => { 46 | throw new Error('Invalid'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /cases/simple-runtypes.ts: -------------------------------------------------------------------------------- 1 | import * as rt from 'simple-runtypes'; 2 | import { addCase } from '../benchmarks'; 3 | 4 | const checkDataSafe = rt.sloppyRecord({ 5 | number: rt.integer(), 6 | negNumber: rt.number(), 7 | maxNumber: rt.number(), 8 | string: rt.string(), 9 | longString: rt.string(), 10 | boolean: rt.boolean(), 11 | deeplyNested: rt.sloppyRecord({ 12 | foo: rt.string(), 13 | num: rt.number(), 14 | bool: rt.boolean(), 15 | }), 16 | }); 17 | 18 | addCase('simple-runtypes', 'parseSafe', data => { 19 | return checkDataSafe(data); 20 | }); 21 | 22 | const checkDataStrict = rt.record({ 23 | number: rt.integer(), 24 | negNumber: rt.number(), 25 | maxNumber: rt.number(), 26 | string: rt.string(), 27 | longString: rt.string(), 28 | boolean: rt.boolean(), 29 | deeplyNested: rt.record({ 30 | foo: rt.string(), 31 | num: rt.number(), 32 | bool: rt.boolean(), 33 | }), 34 | }); 35 | 36 | addCase('simple-runtypes', 'parseStrict', data => { 37 | return checkDataStrict(data); 38 | }); 39 | 40 | addCase('simple-runtypes', 'assertStrict', data => { 41 | checkDataStrict(data); 42 | 43 | return true; 44 | }); 45 | -------------------------------------------------------------------------------- /cases/computed-types.ts: -------------------------------------------------------------------------------- 1 | import Schema, { boolean, number, string } from 'computed-types'; 2 | import { type UnknownData, addCase } from '../benchmarks'; 3 | 4 | const validator = Schema({ 5 | number: number, 6 | negNumber: number.lt(0), 7 | maxNumber: number, 8 | string: string, 9 | longString: string, 10 | boolean: boolean, 11 | deeplyNested: { 12 | foo: string, 13 | num: number, 14 | bool: boolean, 15 | }, 16 | }); 17 | 18 | const validatorStrict = Schema( 19 | { 20 | number: number, 21 | negNumber: number.lt(0), 22 | maxNumber: number, 23 | string: string, 24 | longString: string, 25 | boolean: boolean, 26 | deeplyNested: { 27 | foo: string, 28 | num: number, 29 | bool: boolean, 30 | }, 31 | }, 32 | { strict: true }, 33 | ); 34 | 35 | addCase('computed-types', 'parseSafe', (data: UnknownData) => { 36 | return validator(data); 37 | }); 38 | 39 | addCase('computed-types', 'parseStrict', (data: UnknownData) => { 40 | return validatorStrict(data); 41 | }); 42 | 43 | addCase('computed-types', 'assertStrict', (data: UnknownData) => { 44 | validatorStrict(data); 45 | 46 | return true; 47 | }); 48 | -------------------------------------------------------------------------------- /cases/spectypes/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | object, 3 | number, 4 | string, 5 | boolean, 6 | struct, 7 | merge, 8 | UNSAFE_record, 9 | unknown, 10 | } from 'spectypes'; 11 | 12 | export const parseStrict = object({ 13 | number, 14 | negNumber: number, 15 | maxNumber: number, 16 | string, 17 | longString: string, 18 | boolean, 19 | deeplyNested: object({ 20 | foo: string, 21 | num: number, 22 | bool: boolean, 23 | }), 24 | }); 25 | 26 | export const parseSafe = struct({ 27 | number, 28 | negNumber: number, 29 | maxNumber: number, 30 | string, 31 | longString: string, 32 | boolean, 33 | deeplyNested: struct({ 34 | foo: string, 35 | num: number, 36 | bool: boolean, 37 | }), 38 | }); 39 | 40 | export const assertLoose = merge( 41 | object({ 42 | number, 43 | negNumber: number, 44 | maxNumber: number, 45 | string, 46 | longString: string, 47 | boolean, 48 | deeplyNested: merge( 49 | object({ 50 | foo: string, 51 | num: number, 52 | bool: boolean, 53 | }), 54 | UNSAFE_record(unknown), 55 | ), 56 | }), 57 | UNSAFE_record(unknown), 58 | ); 59 | -------------------------------------------------------------------------------- /cases/mol_data.ts: -------------------------------------------------------------------------------- 1 | import $ from 'mol_data_all'; 2 | 3 | const { 4 | $mol_data_number: Numb, 5 | $mol_data_record: Rec, 6 | $mol_data_string: Str, 7 | $mol_data_boolean: Bool, 8 | } = $; 9 | 10 | import { createCase } from '../benchmarks'; 11 | 12 | createCase('$mol_data', 'parseSafe', () => { 13 | const dataType = Rec({ 14 | number: Numb, 15 | negNumber: Numb, 16 | maxNumber: Numb, 17 | string: Str, 18 | longString: Str, 19 | boolean: Bool, 20 | deeplyNested: Rec({ 21 | foo: Str, 22 | num: Numb, 23 | bool: Bool, 24 | }), 25 | }); 26 | 27 | return data => { 28 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 29 | return dataType(data as any); 30 | }; 31 | }); 32 | 33 | createCase('$mol_data', 'assertLoose', () => { 34 | const dataType = Rec({ 35 | number: Numb, 36 | negNumber: Numb, 37 | maxNumber: Numb, 38 | string: Str, 39 | longString: Str, 40 | boolean: Bool, 41 | deeplyNested: Rec({ 42 | foo: Str, 43 | num: Numb, 44 | bool: Bool, 45 | }), 46 | }); 47 | 48 | return data => { 49 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 50 | dataType(data as any); 51 | return true; 52 | }; 53 | }); 54 | -------------------------------------------------------------------------------- /cases/ts-json-validator.ts: -------------------------------------------------------------------------------- 1 | import { createSchema as S, TsjsonParser } from 'ts-json-validator'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('ts-json-validator', 'assertLoose', () => { 5 | const parser = new TsjsonParser( 6 | S({ 7 | type: 'object', 8 | required: [ 9 | 'boolean', 10 | 'deeplyNested', 11 | 'longString', 12 | 'maxNumber', 13 | 'negNumber', 14 | 'number', 15 | 'string', 16 | ], 17 | properties: { 18 | number: S({ type: 'number' }), 19 | negNumber: S({ type: 'number' }), 20 | maxNumber: S({ type: 'number' }), 21 | string: S({ type: 'string' }), 22 | longString: S({ type: 'string' }), 23 | boolean: S({ type: 'boolean' }), 24 | deeplyNested: S({ 25 | type: 'object', 26 | required: ['foo', 'bool', 'num'], 27 | properties: { 28 | foo: S({ type: 'string' }), 29 | num: S({ type: 'number' }), 30 | bool: S({ type: 'boolean' }), 31 | }, 32 | }), 33 | }, 34 | }), 35 | ); 36 | 37 | return data => { 38 | if (parser.validates(data)) { 39 | return true; 40 | } 41 | 42 | throw new Error('Invalid'); 43 | }; 44 | }); 45 | -------------------------------------------------------------------------------- /cases/sury.ts: -------------------------------------------------------------------------------- 1 | import * as S from 'sury'; 2 | 3 | import { createCase } from '../benchmarks'; 4 | 5 | S.global({ 6 | disableNanNumberValidation: true, 7 | }); 8 | 9 | const schema = S.schema({ 10 | number: S.number, 11 | negNumber: S.number, 12 | maxNumber: S.number, 13 | string: S.string, 14 | longString: S.string, 15 | boolean: S.boolean, 16 | deeplyNested: { 17 | foo: S.string, 18 | num: S.number, 19 | bool: S.boolean, 20 | }, 21 | }); 22 | 23 | createCase('sury', 'parseSafe', () => { 24 | const parseSafe = S.compile(schema, 'Any', 'Output', 'Sync'); 25 | return data => { 26 | return parseSafe(data); 27 | }; 28 | }); 29 | 30 | createCase('sury', 'parseStrict', () => { 31 | const parseStrict = S.compile(S.deepStrict(schema), 'Any', 'Output', 'Sync'); 32 | return data => { 33 | return parseStrict(data); 34 | }; 35 | }); 36 | 37 | createCase('sury', 'assertLoose', () => { 38 | const assertLoose = S.compile(schema, 'Any', 'Assert', 'Sync'); 39 | return data => { 40 | assertLoose(data)!; 41 | return true; 42 | }; 43 | }); 44 | 45 | createCase('sury', 'assertStrict', () => { 46 | const assertStrict = S.compile(S.deepStrict(schema), 'Any', 'Assert', 'Sync'); 47 | return data => { 48 | assertStrict(data)!; 49 | return true; 50 | }; 51 | }); 52 | -------------------------------------------------------------------------------- /cases/decoders.ts: -------------------------------------------------------------------------------- 1 | import { boolean, exact, guard, number, object, string } from 'decoders'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('decoders', 'parseSafe', () => { 5 | const dataType = object({ 6 | number, 7 | negNumber: number, 8 | maxNumber: number, 9 | string, 10 | longString: string, 11 | boolean, 12 | deeplyNested: object({ 13 | foo: string, 14 | num: number, 15 | bool: boolean, 16 | }), 17 | }); 18 | 19 | const dataTypeGuard = guard(dataType); 20 | 21 | return data => { 22 | return dataTypeGuard(data); 23 | }; 24 | }); 25 | 26 | const dataTypeStrict = exact({ 27 | number, 28 | negNumber: number, 29 | maxNumber: number, 30 | string, 31 | longString: string, 32 | boolean, 33 | deeplyNested: exact({ 34 | foo: string, 35 | num: number, 36 | bool: boolean, 37 | }), 38 | }); 39 | 40 | createCase('decoders', 'parseStrict', () => { 41 | const dataTypeGuardStrict = guard(dataTypeStrict); 42 | 43 | return data => { 44 | return dataTypeGuardStrict(data); 45 | }; 46 | }); 47 | 48 | createCase('decoders', 'assertStrict', () => { 49 | const dataTypeGuardStrict = guard(dataTypeStrict); 50 | 51 | return data => { 52 | dataTypeGuardStrict(data); 53 | 54 | return true; 55 | }; 56 | }); 57 | -------------------------------------------------------------------------------- /cases/valibot.ts: -------------------------------------------------------------------------------- 1 | import { object, number, string, boolean, parse, strictObject } from 'valibot'; 2 | import { addCase } from '../benchmarks'; 3 | 4 | const LooseSchema = object({ 5 | number: number(), 6 | negNumber: number(), 7 | maxNumber: number(), 8 | string: string(), 9 | longString: string(), 10 | boolean: boolean(), 11 | deeplyNested: object({ 12 | foo: string(), 13 | num: number(), 14 | bool: boolean(), 15 | }), 16 | }); 17 | 18 | const StrictSchema = strictObject({ 19 | number: number(), 20 | negNumber: number(), 21 | maxNumber: number(), 22 | string: string(), 23 | longString: string(), 24 | boolean: boolean(), 25 | deeplyNested: strictObject({ 26 | foo: string(), 27 | num: number(), 28 | bool: boolean(), 29 | }), 30 | }); 31 | 32 | addCase('valibot', 'assertLoose', data => { 33 | parse(LooseSchema, data, { 34 | abortEarly: true, 35 | }); 36 | return true; 37 | }); 38 | 39 | addCase('valibot', 'assertStrict', data => { 40 | parse(StrictSchema, data, { 41 | abortEarly: true, 42 | }); 43 | return true; 44 | }); 45 | 46 | addCase('valibot', 'parseSafe', data => { 47 | return parse(LooseSchema, data, { 48 | abortEarly: true, 49 | }); 50 | }); 51 | 52 | addCase('valibot', 'parseStrict', data => { 53 | return parse(StrictSchema, data, { 54 | abortEarly: true, 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /benchmarks/helpers/types.ts: -------------------------------------------------------------------------------- 1 | import type { SuiteAPI, ExpectStatic, TestAPI } from 'vitest'; 2 | 3 | export interface BenchmarkCase { 4 | readonly moduleName: string; 5 | 6 | // execute the actual benchmark function 7 | run(): void; 8 | 9 | // run the benchmarks vitest test 10 | test(describe: SuiteAPI, expect: ExpectStatic, test: TestAPI): void; 11 | } 12 | 13 | export abstract class Benchmark implements BenchmarkCase { 14 | // name of the module that is benchmarked 15 | readonly moduleName: string; 16 | 17 | // the function that implements the benchmark 18 | readonly fn: Fn; 19 | 20 | constructor(moduleName: string, fn: Fn) { 21 | this.moduleName = moduleName; 22 | this.fn = fn; 23 | } 24 | 25 | // execute the actual benchmark function 26 | abstract run(): void; 27 | 28 | // run the benchmarks vitest test 29 | abstract test(describe: SuiteAPI, expect: ExpectStatic, test: TestAPI): void; 30 | } 31 | 32 | // Aliased any. 33 | // Need to use ´any` for libraries that do not accept `unknown` as data input 34 | // to their parse/assert functions. 35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 36 | export type UnknownData = any; 37 | 38 | export interface BenchmarkResult { 39 | name: string; 40 | benchmark: string; 41 | runtime: string; 42 | runtimeVersion: string; 43 | ops: number; 44 | margin: number; 45 | } 46 | -------------------------------------------------------------------------------- /cases/rescript-schema.ts: -------------------------------------------------------------------------------- 1 | import * as S from 'rescript-schema'; 2 | 3 | import { createCase } from '../benchmarks'; 4 | 5 | S.setGlobalConfig({ 6 | disableNanNumberValidation: true, 7 | }); 8 | 9 | const schema = S.schema({ 10 | number: S.number, 11 | negNumber: S.number, 12 | maxNumber: S.number, 13 | string: S.string, 14 | longString: S.string, 15 | boolean: S.boolean, 16 | deeplyNested: { 17 | foo: S.string, 18 | num: S.number, 19 | bool: S.boolean, 20 | }, 21 | }); 22 | 23 | createCase('rescript-schema', 'parseSafe', () => { 24 | const parseSafe = S.compile(schema, 'Any', 'Output', 'Sync'); 25 | return data => { 26 | return parseSafe(data); 27 | }; 28 | }); 29 | 30 | createCase('rescript-schema', 'parseStrict', () => { 31 | const parseStrict = S.compile(S.deepStrict(schema), 'Any', 'Output', 'Sync'); 32 | return data => { 33 | return parseStrict(data); 34 | }; 35 | }); 36 | 37 | createCase('rescript-schema', 'assertLoose', () => { 38 | const assertLoose = S.compile(schema, 'Any', 'Assert', 'Sync'); 39 | return data => { 40 | assertLoose(data)!; 41 | return true; 42 | }; 43 | }); 44 | 45 | createCase('rescript-schema', 'assertStrict', () => { 46 | const assertStrict = S.compile(S.deepStrict(schema), 'Any', 'Assert', 'Sync'); 47 | return data => { 48 | assertStrict(data)!; 49 | return true; 50 | }; 51 | }); 52 | -------------------------------------------------------------------------------- /cases/typeofweb-schema.ts: -------------------------------------------------------------------------------- 1 | import { object, number, string, validate, boolean } from '@typeofweb/schema'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('@typeofweb/schema', 'assertLoose', () => { 5 | const dataType = object( 6 | { 7 | number: number(), 8 | negNumber: number(), 9 | maxNumber: number(), 10 | string: string(), 11 | longString: string(), 12 | boolean: boolean(), 13 | deeplyNested: object( 14 | { 15 | foo: string(), 16 | num: number(), 17 | bool: boolean(), 18 | }, 19 | { allowUnknownKeys: true }, 20 | )(), 21 | }, 22 | { allowUnknownKeys: true }, 23 | )(); 24 | 25 | const validator = validate(dataType); 26 | 27 | return data => { 28 | validator(data); 29 | 30 | return true; 31 | }; 32 | }); 33 | 34 | createCase('@typeofweb/schema', 'parseStrict', () => { 35 | const dataType = object({ 36 | number: number(), 37 | negNumber: number(), 38 | maxNumber: number(), 39 | string: string(), 40 | longString: string(), 41 | boolean: boolean(), 42 | deeplyNested: object({ 43 | foo: string(), 44 | num: number(), 45 | bool: boolean(), 46 | })(), 47 | })(); 48 | 49 | const validator = validate(dataType); 50 | 51 | return data => { 52 | return validator(data); 53 | }; 54 | }); 55 | -------------------------------------------------------------------------------- /cases/succulent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | guard, 3 | is, 4 | $boolean, 5 | $Exact, 6 | $interface, 7 | $number, 8 | $string, 9 | } from 'succulent'; 10 | import { createCase } from '../benchmarks'; 11 | 12 | const $LooseType = $interface({ 13 | number: $number, 14 | negNumber: $number, 15 | maxNumber: $number, 16 | string: $string, 17 | longString: $string, 18 | boolean: $boolean, 19 | deeplyNested: $interface({ 20 | foo: $string, 21 | num: $number, 22 | bool: $boolean, 23 | }), 24 | }); 25 | 26 | const $StrictType = $Exact({ 27 | number: $number, 28 | negNumber: $number, 29 | maxNumber: $number, 30 | string: $string, 31 | longString: $string, 32 | boolean: $boolean, 33 | deeplyNested: $Exact({ 34 | foo: $string, 35 | num: $number, 36 | bool: $boolean, 37 | }), 38 | }); 39 | 40 | createCase('succulent', 'parseStrict', () => { 41 | return data => { 42 | const ok = is(data, $StrictType); 43 | if (!ok) { 44 | throw new Error('invalid data'); 45 | } 46 | 47 | return data; 48 | }; 49 | }); 50 | 51 | createCase('succulent', 'assertLoose', () => { 52 | return data => { 53 | guard(data, $LooseType); 54 | 55 | return true; 56 | }; 57 | }); 58 | 59 | createCase('succulent', 'assertStrict', () => { 60 | return data => { 61 | guard(data, $StrictType); 62 | 63 | return true; 64 | }; 65 | }); 66 | -------------------------------------------------------------------------------- /cases/sinclair-typemap-valibot.ts: -------------------------------------------------------------------------------- 1 | import { createCase } from '../benchmarks'; 2 | import { Compile } from '@sinclair/typemap'; 3 | import { object, number, string, boolean, strictObject } from 'valibot'; 4 | 5 | const LooseSchema = Compile( 6 | object({ 7 | number: number(), 8 | negNumber: number(), 9 | maxNumber: number(), 10 | string: string(), 11 | longString: string(), 12 | boolean: boolean(), 13 | deeplyNested: object({ 14 | foo: string(), 15 | num: number(), 16 | bool: boolean(), 17 | }), 18 | }), 19 | ); 20 | 21 | const StrictSchema = Compile( 22 | strictObject({ 23 | number: number(), 24 | negNumber: number(), 25 | maxNumber: number(), 26 | string: string(), 27 | longString: string(), 28 | boolean: boolean(), 29 | deeplyNested: strictObject({ 30 | foo: string(), 31 | num: number(), 32 | bool: boolean(), 33 | }), 34 | }), 35 | ); 36 | 37 | createCase('@sinclair/typemap/valibot', 'assertLoose', () => { 38 | return data => { 39 | if (!LooseSchema.Check(data)) { 40 | throw new Error('validation failure'); 41 | } 42 | return true; 43 | }; 44 | }); 45 | createCase('@sinclair/typemap/valibot', 'assertStrict', () => { 46 | return data => { 47 | if (!StrictSchema.Check(data)) { 48 | throw new Error('validation failure'); 49 | } 50 | return true; 51 | }; 52 | }); 53 | -------------------------------------------------------------------------------- /cases/vality.ts: -------------------------------------------------------------------------------- 1 | import { v, validate } from 'vality'; 2 | import { addCase } from '../benchmarks'; 3 | 4 | const dataType = v.object({ 5 | number: v.number, 6 | negNumber: v.number, 7 | maxNumber: v.number({ allowUnsafe: true }), 8 | string: v.string, 9 | longString: v.string, 10 | boolean: v.boolean, 11 | deeplyNested: v.object({ 12 | foo: v.string, 13 | num: v.number, 14 | bool: v.boolean, 15 | }), 16 | }); 17 | 18 | addCase('vality', 'parseSafe', data => { 19 | const res = validate(dataType, data, { bail: true, strict: true }); 20 | 21 | if (res.valid) return res.data; 22 | throw new Error('Invalid!'); 23 | }); 24 | 25 | addCase('vality', 'parseStrict', data => { 26 | const res = validate(dataType, data, { 27 | bail: true, 28 | strict: true, 29 | allowExtraProperties: false, 30 | }); 31 | 32 | if (res.valid) return res.data; 33 | throw new Error('Invalid!'); 34 | }); 35 | 36 | addCase('vality', 'assertLoose', data => { 37 | const res = validate(dataType, data, { bail: true, strict: true }); 38 | 39 | if (res.valid) return true; 40 | throw new Error('Invalid!'); 41 | }); 42 | 43 | addCase('vality', 'assertStrict', data => { 44 | const res = validate(dataType, data, { 45 | bail: true, 46 | strict: true, 47 | allowExtraProperties: false, 48 | }); 49 | 50 | if (res.valid) return true; 51 | throw new Error('Invalid!'); 52 | }); 53 | -------------------------------------------------------------------------------- /cases/io-ts.ts: -------------------------------------------------------------------------------- 1 | import { fold } from 'fp-ts/Either'; 2 | import { pipe } from 'fp-ts/function'; 3 | import * as t from 'io-ts'; 4 | import { createCase } from '../benchmarks'; 5 | 6 | createCase('io-ts', 'assertLoose', () => { 7 | const dataType = t.type({ 8 | number: t.Int, 9 | negNumber: t.number, 10 | maxNumber: t.number, 11 | string: t.string, 12 | longString: t.string, 13 | boolean: t.boolean, 14 | deeplyNested: t.type({ 15 | foo: t.string, 16 | num: t.number, 17 | bool: t.boolean, 18 | }), 19 | }); 20 | 21 | return data => { 22 | return pipe( 23 | dataType.decode(data), 24 | fold( 25 | errors => { 26 | throw errors; 27 | }, 28 | () => true, 29 | ), 30 | ); 31 | }; 32 | }); 33 | 34 | createCase('io-ts', 'parseSafe', () => { 35 | const dataType = t.strict({ 36 | number: t.Int, 37 | negNumber: t.number, 38 | maxNumber: t.number, 39 | string: t.string, 40 | longString: t.string, 41 | boolean: t.boolean, 42 | deeplyNested: t.strict({ 43 | foo: t.string, 44 | num: t.number, 45 | bool: t.boolean, 46 | }), 47 | }); 48 | 49 | return data => { 50 | return pipe( 51 | dataType.decode(data), 52 | fold( 53 | errors => { 54 | throw errors; 55 | }, 56 | result => result, 57 | ), 58 | ); 59 | }; 60 | }); 61 | -------------------------------------------------------------------------------- /cases/myzod.ts: -------------------------------------------------------------------------------- 1 | import myzod from 'myzod'; 2 | import { addCase, createCase } from '../benchmarks'; 3 | 4 | createCase('myzod', 'parseSafe', () => { 5 | const dataType = myzod.object( 6 | { 7 | number: myzod.number(), 8 | negNumber: myzod.number(), 9 | maxNumber: myzod.number(), 10 | string: myzod.string(), 11 | longString: myzod.string(), 12 | boolean: myzod.boolean(), 13 | deeplyNested: myzod.object( 14 | { 15 | foo: myzod.string(), 16 | num: myzod.number(), 17 | bool: myzod.boolean(), 18 | }, 19 | { 20 | allowUnknown: true, 21 | }, 22 | ), 23 | }, 24 | { 25 | allowUnknown: true, 26 | }, 27 | ); 28 | 29 | return data => { 30 | return dataType.parse(data); 31 | }; 32 | }); 33 | 34 | const dataTypeStrict = myzod.object({ 35 | number: myzod.number(), 36 | negNumber: myzod.number(), 37 | maxNumber: myzod.number(), 38 | string: myzod.string(), 39 | longString: myzod.string(), 40 | boolean: myzod.boolean(), 41 | deeplyNested: myzod.object({ 42 | foo: myzod.string(), 43 | num: myzod.number(), 44 | bool: myzod.boolean(), 45 | }), 46 | }); 47 | 48 | addCase('myzod', 'parseStrict', data => { 49 | return dataTypeStrict.parse(data); 50 | }); 51 | 52 | addCase('myzod', 'assertStrict', data => { 53 | dataTypeStrict.parse(data); 54 | 55 | return true; 56 | }); 57 | -------------------------------------------------------------------------------- /cases/deepkit/build/index.d.ts: -------------------------------------------------------------------------------- 1 | interface ToBeChecked { 2 | number: number; 3 | negNumber: number; 4 | maxNumber: number; 5 | string: string; 6 | longString: string; 7 | boolean: boolean; 8 | deeplyNested: { 9 | foo: string; 10 | num: number; 11 | bool: boolean; 12 | }; 13 | } 14 | /** 15 | * Check that an object conforms to the schema. 16 | * 17 | * Ignore any extra keys in input objects. 18 | * 19 | * Such a validation mode is highly unsafe when used on untrusted input. 20 | * 21 | * But not checking for unknown/extra keys in records may provide massive 22 | * speedups and may suffice in certain scenarios. 23 | */ 24 | export declare function assertLoose(input: unknown): boolean; 25 | /** 26 | * Check that an object conforms to the schema. 27 | * 28 | * Raise errors if any extra keys not present in the schema are found. 29 | */ 30 | export declare function assertStrict(): boolean; 31 | /** 32 | * Like parseSafe but throw on unknown (extra) keys in objects. 33 | */ 34 | export declare function parseStrict(): ToBeChecked; 35 | /** 36 | * Validate and ignore unknown keys, removing them from the result. 37 | * 38 | * When validating untrusted data, unknown keys should always be removed to 39 | * not result in unwanted parameters or the `__proto__` attribute being 40 | * maliciously passed to internal functions. 41 | */ 42 | export declare function parseSafe(input: unknown): ToBeChecked; 43 | export {}; 44 | -------------------------------------------------------------------------------- /cases/yup.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('yup', 'assertLoose', () => { 5 | const dataType = yup.object({ 6 | number: yup.number().required(), 7 | negNumber: yup.number().required(), 8 | maxNumber: yup.number().required(), 9 | string: yup.string().required(), 10 | longString: yup.string().required(), 11 | boolean: yup.bool().required(), 12 | deeplyNested: yup.object({ 13 | foo: yup.string().required(), 14 | num: yup.number().required(), 15 | bool: yup.bool().required(), 16 | }), 17 | }); 18 | 19 | return data => { 20 | const res = dataType.isValidSync(data, { recursive: true, strict: false }); 21 | 22 | if (!res) { 23 | throw new Error('invalid'); 24 | } 25 | 26 | return res; 27 | }; 28 | }); 29 | 30 | createCase('yup', 'parseSafe', () => { 31 | const dataType = yup.object({ 32 | number: yup.number().required(), 33 | negNumber: yup.number().required(), 34 | maxNumber: yup.number().required(), 35 | string: yup.string().required(), 36 | longString: yup.string().required(), 37 | boolean: yup.bool().required(), 38 | deeplyNested: yup.object({ 39 | foo: yup.string().required(), 40 | num: yup.number().required(), 41 | bool: yup.bool().required(), 42 | }), 43 | }); 44 | 45 | return data => { 46 | return dataType.validateSync(data, { 47 | recursive: true, 48 | strict: false, 49 | stripUnknown: true, 50 | }); 51 | }; 52 | }); 53 | -------------------------------------------------------------------------------- /cases/class-validator.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { transformAndValidateSync } from 'class-transformer-validator'; 3 | import { 4 | IsBoolean, 5 | IsNegative, 6 | IsNumber, 7 | IsString, 8 | ValidateNested, 9 | } from 'class-validator'; 10 | import 'reflect-metadata'; 11 | import { createCase } from '../benchmarks'; 12 | 13 | createCase('class-transformer-validator-sync', 'assertLoose', () => { 14 | class DeeplyNestedType { 15 | @IsString() 16 | foo!: string; 17 | 18 | @IsNumber() 19 | num!: number; 20 | 21 | @IsBoolean() 22 | bool!: boolean; 23 | } 24 | 25 | class DataType { 26 | @IsNumber() 27 | number!: number; 28 | 29 | @IsNegative() 30 | negNumber!: number; 31 | 32 | @IsNumber() 33 | maxNumber!: number; 34 | 35 | @IsString() 36 | string!: string; 37 | 38 | @IsString() 39 | longString!: string; 40 | 41 | @IsBoolean() 42 | boolean!: boolean; 43 | 44 | @ValidateNested() 45 | @Type(() => DeeplyNestedType) 46 | deeplyNested!: DeeplyNestedType; 47 | } 48 | 49 | return data => { 50 | // We cast the data as some "unknown" object, to make sure that it does not bias the validator 51 | // We are not using "any" type here, because that confuses "class-validator", as it can also 52 | // work on arrays, and it returns ambiguous "Foo | Foo[]" type if it doesn't know if input was 53 | // an array or not. 54 | transformAndValidateSync(DataType, data as {}); 55 | 56 | return true; 57 | }; 58 | }); 59 | -------------------------------------------------------------------------------- /cases/superstruct.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assert, 3 | boolean, 4 | mask, 5 | number, 6 | object, 7 | string, 8 | type, 9 | } from 'superstruct'; 10 | import { addCase } from '../benchmarks'; 11 | 12 | const dataTypeSafe = type({ 13 | number: number(), 14 | negNumber: number(), 15 | maxNumber: number(), 16 | string: string(), 17 | longString: string(), 18 | boolean: boolean(), 19 | deeplyNested: type({ 20 | foo: string(), 21 | num: number(), 22 | bool: boolean(), 23 | }), 24 | }); 25 | 26 | const dataTypeStrict = object({ 27 | number: number(), 28 | negNumber: number(), 29 | maxNumber: number(), 30 | string: string(), 31 | longString: string(), 32 | boolean: boolean(), 33 | deeplyNested: object({ 34 | foo: string(), 35 | num: number(), 36 | bool: boolean(), 37 | }), 38 | }); 39 | 40 | addCase( 41 | 'superstruct', 42 | 'parseSafe', 43 | data => { 44 | assert(data, dataTypeSafe); 45 | 46 | return mask(data, dataTypeSafe); 47 | }, 48 | // can't get the `mask` stuff to work - its documented to remove any 49 | // additional attributes that `type` ignored 50 | { disabled: true }, 51 | ); 52 | 53 | addCase('superstruct', 'parseStrict', data => { 54 | assert(data, dataTypeStrict); 55 | 56 | return data; 57 | }); 58 | 59 | addCase('superstruct', 'assertLoose', data => { 60 | assert(data, dataTypeSafe); 61 | 62 | return true; 63 | }); 64 | 65 | addCase('superstruct', 'assertStrict', data => { 66 | assert(data, dataTypeStrict); 67 | 68 | return true; 69 | }); 70 | -------------------------------------------------------------------------------- /cases/index.ts: -------------------------------------------------------------------------------- 1 | export const cases = [ 2 | 'aeria', 3 | 'ajv', 4 | 'arktype', 5 | 'banditypes', 6 | 'bueno', 7 | 'caketype', 8 | 'class-validator', 9 | 'cleaners', 10 | 'computed-types', 11 | 'decoders', 12 | 'io-ts', 13 | 'joi', 14 | 'jointz', 15 | 'json-decoder', 16 | 'mol_data', 17 | 'mojotech-json-type-validation', 18 | 'mondrian-framework', 19 | 'myzod', 20 | 'ok-computer', 21 | 'parse-dont-validate', 22 | 'paseri', 23 | 'pure-parse', 24 | 'purify-ts', 25 | 'r-assign', 26 | 'rescript-schema', 27 | 'rulr', 28 | 'runtypes', 29 | 'sapphire-shapeshift', 30 | 'simple-runtypes', 31 | 'sinclair-typebox-ahead-of-time', 32 | 'sinclair-typebox-dynamic', 33 | 'sinclair-typebox-just-in-time', 34 | 'sinclair-typemap-valibot', 35 | 'sinclair-typemap-zod', 36 | 'spectypes', 37 | 'stnl', 38 | 'succulent', 39 | 'superstruct', 40 | 'suretype', 41 | 'sury', 42 | 'tiny-schema-validator', 43 | 'to-typed', 44 | 'toi', 45 | 'ts-interface-checker', 46 | 'ts-json-validator', 47 | 'ts-runtime-checks', 48 | 'ts-utils', 49 | 'tson', 50 | 'typeofweb-schema', 51 | 'typia', 52 | 'unknownutil', 53 | 'valibot', 54 | 'valita', 55 | 'vality', 56 | 'yup', 57 | 'zod', 58 | 'zod4', 59 | 'deepkit', 60 | 'effect-schema', 61 | 'ts-auto-guard', 62 | 'type-predicate-generator', 63 | 'jet-validators', 64 | ] as const; 65 | 66 | export type CaseName = (typeof cases)[number]; 67 | 68 | export async function importCase(caseName: CaseName) { 69 | await import('./' + caseName); 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | # from: https://github.com/actions/starter-workflows/blob/main/pages/static.yml 2 | name: Deploy Github Pages 3 | 4 | on: 5 | push: 6 | branches: [master] 7 | 8 | workflow_dispatch: 9 | 10 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 17 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: false 21 | 22 | jobs: 23 | # Single deploy job since we're just deploying 24 | deploy: 25 | runs-on: ubuntu-latest 26 | 27 | defaults: 28 | run: 29 | working-directory: "./docs" 30 | 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | 35 | - name: Use latest Node.js 36 | uses: actions/setup-node@v4 37 | 38 | - name: Install 39 | run: npm ci 40 | 41 | - name: Lint 42 | run: npm run lint 43 | 44 | - name: Build docs 45 | run: npm run build 46 | 47 | - name: Setup Pages 48 | uses: actions/configure-pages@v5 49 | 50 | - name: Upload artifact 51 | uses: actions/upload-pages-artifact@v3 52 | with: 53 | path: "./docs/dist" 54 | 55 | - name: Deploy to GitHub Pages 56 | id: deployment 57 | uses: actions/deploy-pages@v4 58 | -------------------------------------------------------------------------------- /cases/sinclair-typemap-zod.ts: -------------------------------------------------------------------------------- 1 | import { createCase } from '../benchmarks'; 2 | import { Compile } from '@sinclair/typemap'; 3 | import * as z from 'zod'; 4 | 5 | const LooseSchema = Compile( 6 | z 7 | .object({ 8 | number: z.number(), 9 | negNumber: z.number(), 10 | maxNumber: z.number(), 11 | string: z.string(), 12 | longString: z.string(), 13 | boolean: z.boolean(), 14 | deeplyNested: z 15 | .object({ 16 | foo: z.string(), 17 | num: z.number(), 18 | bool: z.boolean(), 19 | }) 20 | .passthrough(), 21 | }) 22 | .passthrough(), 23 | ); 24 | 25 | const StrictSchema = Compile( 26 | z 27 | .object({ 28 | number: z.number(), 29 | negNumber: z.number(), 30 | maxNumber: z.number(), 31 | string: z.string(), 32 | longString: z.string(), 33 | boolean: z.boolean(), 34 | deeplyNested: z 35 | .object({ 36 | foo: z.string(), 37 | num: z.number(), 38 | bool: z.boolean(), 39 | }) 40 | .strict(), 41 | }) 42 | .strict(), 43 | ); 44 | 45 | createCase('@sinclair/typemap/zod', 'assertLoose', () => { 46 | return data => { 47 | if (!LooseSchema.Check(data)) { 48 | throw new Error('validation failure'); 49 | } 50 | return true; 51 | }; 52 | }); 53 | createCase('@sinclair/typemap/zod', 'assertStrict', () => { 54 | return data => { 55 | if (!StrictSchema.Check(data)) { 56 | throw new Error('validation failure'); 57 | } 58 | return true; 59 | }; 60 | }); 61 | -------------------------------------------------------------------------------- /docs/prepareResults.mjs: -------------------------------------------------------------------------------- 1 | // Script to make the benchmark result files accessible to the benchmark viewer app 2 | // 3 | // - keeps them at their current place for easier access tp benchmark 4 | // history data in the future 5 | // - maybe preprocess and combine them into 1 single file? 6 | import * as fs from 'fs'; 7 | import * as path from 'path'; 8 | 9 | function copyFileSync(src, dest) { 10 | console.log('copying', src, 'to', dest); 11 | fs.copyFileSync(src, dest); 12 | } 13 | 14 | /* benchmark results */ 15 | 16 | // older benchmark app builds where based on publishing the whole 17 | // docs folder with github pages 18 | const sourceDir = 'results'; 19 | 20 | // everything in public can be fetched from the app 21 | const destDir = 'public/results'; 22 | 23 | if (!fs.existsSync(sourceDir)) { 24 | console.error('does not exist:', sourceDir); 25 | process.exit(1); 26 | } 27 | 28 | if (!fs.existsSync(destDir)) { 29 | fs.mkdirSync(destDir, { recursive: true }); 30 | } 31 | 32 | const files = fs.readdirSync(sourceDir); 33 | 34 | files.forEach(file => { 35 | const sourcePath = path.join(sourceDir, file); 36 | const destPath = path.join(destDir, file); 37 | const stats = fs.statSync(sourcePath); 38 | 39 | if (stats.isDirectory()) { 40 | console.error('did not expect a directory here:', destPath); 41 | process.exit(1); 42 | } 43 | 44 | copyFileSync(sourcePath, destPath); 45 | }); 46 | 47 | /* package popularity file */ 48 | 49 | copyFileSync('packagesPopularity.json', 'public/packagesPopularity.json'); 50 | 51 | console.log('done'); 52 | process.exit(0); 53 | -------------------------------------------------------------------------------- /cases/unknownutil.ts: -------------------------------------------------------------------------------- 1 | import { is, ensure, assert } from 'unknownutil'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | const dataTypeLoose = is.ObjectOf({ 5 | number: is.Number, 6 | negNumber: is.Number, 7 | maxNumber: is.Number, 8 | string: is.String, 9 | longString: is.String, 10 | boolean: is.Boolean, 11 | deeplyNested: is.ObjectOf({ 12 | foo: is.String, 13 | num: is.Number, 14 | bool: is.Boolean, 15 | }), 16 | }); 17 | 18 | const dataTypeStrict = is.ObjectOf( 19 | { 20 | number: is.Number, 21 | negNumber: is.Number, 22 | maxNumber: is.Number, 23 | string: is.String, 24 | longString: is.String, 25 | boolean: is.Boolean, 26 | deeplyNested: is.ObjectOf( 27 | { 28 | foo: is.String, 29 | num: is.Number, 30 | bool: is.Boolean, 31 | }, 32 | { strict: true }, 33 | ), 34 | }, 35 | { strict: true }, 36 | ); 37 | 38 | // TODO: unklike other validators, unknownutil does not remove extra properties 39 | // createCase('unknownutil', 'parseSafe', () => { 40 | // return data => { 41 | // return ensure(data, dataTypeLoose); 42 | // }; 43 | // }); 44 | 45 | createCase('unknownutil', 'parseStrict', () => { 46 | return data => { 47 | return ensure(data, dataTypeStrict); 48 | }; 49 | }); 50 | 51 | createCase('unknownutil', 'assertStrict', () => { 52 | return data => { 53 | assert(data, dataTypeStrict); 54 | return true; 55 | }; 56 | }); 57 | 58 | createCase('unknownutil', 'assertLoose', () => { 59 | return data => { 60 | assert(data, dataTypeLoose); 61 | return true; 62 | }; 63 | }); 64 | -------------------------------------------------------------------------------- /cases/joi.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | const schema = Joi.object({ 5 | number: Joi.number().required(), 6 | negNumber: Joi.number().required(), 7 | maxNumber: Joi.number().unsafe().required(), 8 | string: Joi.string().required(), 9 | longString: Joi.string().required(), 10 | boolean: Joi.boolean().required(), 11 | deeplyNested: Joi.object({ 12 | foo: Joi.string().required(), 13 | num: Joi.number().required(), 14 | bool: Joi.boolean().required(), 15 | }).required(), 16 | }); 17 | 18 | createCase('joi', 'parseSafe', () => { 19 | return data => { 20 | const { value, error } = schema.validate(data, { 21 | stripUnknown: true, 22 | allowUnknown: true, 23 | convert: false, 24 | }); 25 | 26 | if (error) throw error; 27 | 28 | return value; 29 | }; 30 | }); 31 | 32 | createCase('joi', 'parseStrict', () => { 33 | return data => { 34 | const { value, error } = schema.validate(data, { 35 | allowUnknown: false, 36 | convert: false, 37 | }); 38 | 39 | if (error) throw error; 40 | 41 | return value; 42 | }; 43 | }); 44 | 45 | createCase('joi', 'assertLoose', () => { 46 | return data => { 47 | const { error } = schema.validate(data, { 48 | stripUnknown: false, 49 | convert: false, 50 | allowUnknown: true, 51 | }); 52 | if (error) throw error; 53 | return true; 54 | }; 55 | }); 56 | 57 | createCase('joi', 'assertStrict', () => { 58 | const strictSchema = schema.options({ convert: false }); 59 | return data => { 60 | const { error } = strictSchema.validate(data, { 61 | convert: false, 62 | allowUnknown: false, 63 | }); 64 | if (error) throw error; 65 | return true; 66 | }; 67 | }); 68 | -------------------------------------------------------------------------------- /cases/toi.ts: -------------------------------------------------------------------------------- 1 | import * as toi from '@toi/toi'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('toi', 'parseStrict', () => { 5 | const obj = () => toi.required().and(toi.obj.isplain()); 6 | const req = () => toi.required(); 7 | const num = () => toi.num.is(); 8 | const str = () => toi.str.is(); 9 | 10 | const isValid = obj().and( 11 | toi.obj.keys({ 12 | number: req().and(num()), 13 | negNumber: req().and(num()), 14 | maxNumber: req().and(num()), 15 | string: req().and(str()), 16 | longString: req().and(str()), 17 | boolean: req().and(toi.bool.is()), 18 | deeplyNested: obj().and( 19 | toi.obj.keys({ 20 | foo: req().and(str()), 21 | num: req().and(num()), 22 | bool: req().and(toi.bool.is()), 23 | }), 24 | ), 25 | }), 26 | ); 27 | 28 | return data => { 29 | isValid(data); 30 | 31 | return data; 32 | }; 33 | }); 34 | 35 | createCase('toi', 'assertStrict', () => { 36 | const obj = () => toi.required().and(toi.obj.isplain()); 37 | const req = () => toi.required(); 38 | const num = () => toi.num.is(); 39 | const str = () => toi.str.is(); 40 | 41 | const isValid = obj().and( 42 | toi.obj.keys({ 43 | number: req().and(num()), 44 | negNumber: req().and(num()), 45 | maxNumber: req().and(num()), 46 | string: req().and(str()), 47 | longString: req().and(str()), 48 | boolean: req().and(toi.bool.is()), 49 | deeplyNested: obj().and( 50 | toi.obj.keys({ 51 | foo: req().and(str()), 52 | num: req().and(num()), 53 | bool: req().and(toi.bool.is()), 54 | }), 55 | ), 56 | }), 57 | ); 58 | 59 | return data => { 60 | isValid(data); 61 | 62 | return true; 63 | }; 64 | }); 65 | -------------------------------------------------------------------------------- /cases/paseri/index.ts: -------------------------------------------------------------------------------- 1 | import * as p from './build'; 2 | import { addCase } from '../../benchmarks'; 3 | 4 | const dataTypeStrip = p 5 | .object({ 6 | number: p.number(), 7 | negNumber: p.number(), 8 | maxNumber: p.number(), 9 | string: p.string(), 10 | longString: p.string(), 11 | boolean: p.boolean(), 12 | deeplyNested: p 13 | .object({ 14 | foo: p.string(), 15 | num: p.number(), 16 | bool: p.boolean(), 17 | }) 18 | .strip(), 19 | }) 20 | .strip(); 21 | 22 | const dataTypeStrict = p 23 | .object({ 24 | number: p.number(), 25 | negNumber: p.number(), 26 | maxNumber: p.number(), 27 | string: p.string(), 28 | longString: p.string(), 29 | boolean: p.boolean(), 30 | deeplyNested: p 31 | .object({ 32 | foo: p.string(), 33 | num: p.number(), 34 | bool: p.boolean(), 35 | }) 36 | .strict(), 37 | }) 38 | .strict(); 39 | 40 | const dataTypePassthrough = p 41 | .object({ 42 | number: p.number(), 43 | negNumber: p.number(), 44 | maxNumber: p.number(), 45 | string: p.string(), 46 | longString: p.string(), 47 | boolean: p.boolean(), 48 | deeplyNested: p 49 | .object({ 50 | foo: p.string(), 51 | num: p.number(), 52 | bool: p.boolean(), 53 | }) 54 | .passthrough(), 55 | }) 56 | .passthrough(); 57 | 58 | addCase('paseri', 'parseSafe', data => { 59 | return dataTypeStrip.parse(data); 60 | }); 61 | 62 | addCase('paseri', 'parseStrict', data => { 63 | return dataTypeStrict.parse(data); 64 | }); 65 | 66 | addCase('paseri', 'assertLoose', data => { 67 | dataTypePassthrough.parse(data); 68 | 69 | return true; 70 | }); 71 | 72 | addCase('paseri', 'assertStrict', data => { 73 | dataTypeStrict.parse(data); 74 | 75 | return true; 76 | }); 77 | -------------------------------------------------------------------------------- /cases/deepkit/src/index.ts: -------------------------------------------------------------------------------- 1 | import { castFunction, getValidatorFunction } from '@deepkit/type'; 2 | 3 | interface ToBeChecked { 4 | number: number; 5 | negNumber: number; 6 | maxNumber: number; 7 | string: string; 8 | longString: string; 9 | boolean: boolean; 10 | deeplyNested: { 11 | foo: string; 12 | num: number; 13 | bool: boolean; 14 | }; 15 | } 16 | 17 | const isToBeChecked = getValidatorFunction(); 18 | const safeToBeChecked = castFunction(); 19 | 20 | /** 21 | * Check that an object conforms to the schema. 22 | * 23 | * Ignore any extra keys in input objects. 24 | * 25 | * Such a validation mode is highly unsafe when used on untrusted input. 26 | * 27 | * But not checking for unknown/extra keys in records may provide massive 28 | * speedups and may suffice in certain scenarios. 29 | */ 30 | export function assertLoose(input: unknown): boolean { 31 | if (!isToBeChecked(input) as boolean) throw new Error('wrong type.'); 32 | return true; 33 | } 34 | 35 | /** 36 | * Check that an object conforms to the schema. 37 | * 38 | * Raise errors if any extra keys not present in the schema are found. 39 | */ 40 | export function assertStrict(): boolean { 41 | throw new Error('not supported.'); 42 | } 43 | 44 | /** 45 | * Like parseSafe but throw on unknown (extra) keys in objects. 46 | */ 47 | export function parseStrict(): ToBeChecked { 48 | throw new Error('not supported.'); 49 | } 50 | 51 | /** 52 | * Validate and ignore unknown keys, removing them from the result. 53 | * 54 | * When validating untrusted data, unknown keys should always be removed to 55 | * not result in unwanted parameters or the `__proto__` attribute being 56 | * maliciously passed to internal functions. 57 | */ 58 | export function parseSafe(input: unknown): ToBeChecked { 59 | return safeToBeChecked(input); 60 | } 61 | -------------------------------------------------------------------------------- /benchmarks/parseStrict.ts: -------------------------------------------------------------------------------- 1 | import { Benchmark } from './helpers/types'; 2 | import { validateData } from './parseSafe'; 3 | import type { ExpectStatic, SuiteAPI, TestAPI } from 'vitest'; 4 | 5 | type Fn = (data: unknown) => typeof validateData; 6 | 7 | /** 8 | * Like parseSafe but throw on unknown (extra) keys in objects. 9 | */ 10 | export class ParseStrict extends Benchmark { 11 | run() { 12 | this.fn(validateData); 13 | } 14 | 15 | test(describe: SuiteAPI, expect: ExpectStatic, test: TestAPI) { 16 | describe(this.moduleName, () => { 17 | test('should validate the data', () => { 18 | expect(this.fn(validateData)).toEqual(validateData); 19 | }); 20 | 21 | test('should throw on invalid attribute type', () => { 22 | expect(() => 23 | this.fn({ 24 | ...validateData, 25 | number: 'foo', 26 | }), 27 | ).toThrow(); 28 | }); 29 | 30 | test('should throw on extra attributes', () => { 31 | expect(() => 32 | this.fn({ 33 | ...validateData, 34 | extraAttribute: true, 35 | }), 36 | ).toThrow(); 37 | }); 38 | 39 | test('should throw on extra nested attributes', () => { 40 | expect(() => 41 | this.fn({ 42 | ...validateData, 43 | deeplyNested: { 44 | ...validateData.deeplyNested, 45 | extraDeepAttribute: true, 46 | }, 47 | }), 48 | ).toThrow(); 49 | }); 50 | 51 | test('should throw on missing attributes', () => { 52 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | const data: any = { 54 | ...validateData, 55 | }; 56 | 57 | delete data.number; 58 | 59 | expect(() => this.fn(data)).toThrow(); 60 | }); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cases/sinclair-typebox.ts: -------------------------------------------------------------------------------- 1 | import { TypeSystemPolicy } from '@sinclair/typebox/system'; 2 | import { Type } from '@sinclair/typebox'; 3 | 4 | // ┌──[TypeScriptPolicy]──────────────────────────────────────────────────────────┐ 5 | // │ │ 6 | // │ const x: {} = [] - Allowed in TypeScript Strict │ 7 | // │ │ 8 | // │ const x: number = NaN - Allowed in TypeScript Strict │ 9 | // │ │ 10 | // └──────────────────────────────────────────────────────────────────────────────┘ 11 | 12 | TypeSystemPolicy.AllowArrayObject = true; // match: typia, ts-runtime-checks 13 | TypeSystemPolicy.AllowNaN = true; // match: valita, typia, ts-runtime-checks, to-typed, spectypes, @sapphire/shapeshift, ok-computer, myzod, jointz, computed-types, bueno 14 | 15 | export const Loose = Type.Object({ 16 | number: Type.Number(), 17 | negNumber: Type.Number(), 18 | maxNumber: Type.Number(), 19 | string: Type.String(), 20 | longString: Type.String(), 21 | boolean: Type.Boolean(), 22 | deeplyNested: Type.Object({ 23 | foo: Type.String(), 24 | num: Type.Number(), 25 | bool: Type.Boolean(), 26 | }), 27 | }); 28 | export const Strict = Type.Object( 29 | { 30 | number: Type.Number(), 31 | negNumber: Type.Number(), 32 | maxNumber: Type.Number(), 33 | string: Type.String(), 34 | longString: Type.String(), 35 | boolean: Type.Boolean(), 36 | deeplyNested: Type.Object( 37 | { 38 | foo: Type.String(), 39 | num: Type.Number(), 40 | bool: Type.Boolean(), 41 | }, 42 | { additionalProperties: false }, 43 | ), 44 | }, 45 | { additionalProperties: false }, 46 | ); 47 | -------------------------------------------------------------------------------- /cases/aeria/index.ts: -------------------------------------------------------------------------------- 1 | import { validate } from '@aeriajs/validation'; 2 | import { createCase } from '../../benchmarks'; 3 | 4 | const schema = { 5 | type: 'object', 6 | properties: { 7 | number: { 8 | type: 'number', 9 | }, 10 | negNumber: { 11 | type: 'number', 12 | }, 13 | maxNumber: { 14 | type: 'number', 15 | }, 16 | string: { 17 | type: 'string', 18 | }, 19 | longString: { 20 | type: 'string', 21 | }, 22 | boolean: { 23 | type: 'boolean', 24 | }, 25 | deeplyNested: { 26 | type: 'object', 27 | properties: { 28 | foo: { 29 | type: 'string', 30 | }, 31 | num: { 32 | type: 'number', 33 | }, 34 | bool: { 35 | type: 'boolean', 36 | }, 37 | }, 38 | required: ['foo', 'num', 'bool'], 39 | }, 40 | }, 41 | required: [ 42 | 'number', 43 | 'negNumber', 44 | 'maxNumber', 45 | 'string', 46 | 'longString', 47 | 'boolean', 48 | 'deeplyNested', 49 | ], 50 | } as const; 51 | 52 | createCase('aeria', 'parseSafe', () => { 53 | return (data: unknown) => { 54 | return validate(data, schema, { 55 | throwOnError: true, 56 | tolerateExtraneous: true, 57 | }).result; 58 | }; 59 | }); 60 | 61 | createCase('aeria', 'parseStrict', () => { 62 | return (data: unknown) => { 63 | return validate(data, schema, { 64 | throwOnError: true, 65 | }).result; 66 | }; 67 | }); 68 | 69 | createCase('aeria', 'assertLoose', () => { 70 | return (data: unknown) => { 71 | validate(data, schema, { 72 | throwOnError: true, 73 | tolerateExtraneous: true, 74 | }); 75 | 76 | return true; 77 | }; 78 | }); 79 | 80 | createCase('aeria', 'assertStrict', () => { 81 | return (data: unknown) => { 82 | validate(data, schema, { 83 | throwOnError: true, 84 | }); 85 | 86 | return true; 87 | }; 88 | }); 89 | -------------------------------------------------------------------------------- /docs/packagesPopularity.json: -------------------------------------------------------------------------------- 1 | [{"name":"aeria","weeklyDownloads":417},{"name":"ajv","weeklyDownloads":164797644},{"name":"arktype","weeklyDownloads":413970},{"name":"banditypes","weeklyDownloads":294},{"name":"bueno","weeklyDownloads":142},{"name":"caketype","weeklyDownloads":61},{"name":"class-transformer-validator-sync","weeklyDownloads":5878956},{"name":"computed-types","weeklyDownloads":2508},{"name":"decoders","weeklyDownloads":22288},{"name":"io-ts","weeklyDownloads":1851059},{"name":"jointz","weeklyDownloads":151},{"name":"json-decoder","weeklyDownloads":178},{"name":"$mol_data","weeklyDownloads":1302},{"name":"@mojotech/json-type-validation","weeklyDownloads":34592},{"name":"mondrian-framework","weeklyDownloads":990},{"name":"myzod","weeklyDownloads":18319},{"name":"ok-computer","weeklyDownloads":194},{"name":"parse-dont-validate (chained function)","weeklyDownloads":156},{"name":"parse-dont-validate (named parameters)","weeklyDownloads":156},{"name":"purify-ts","weeklyDownloads":41947},{"name":"r-assign","weeklyDownloads":141},{"name":"rescript-schema","weeklyDownloads":3627},{"name":"rulr","weeklyDownloads":1336},{"name":"runtypes","weeklyDownloads":206772},{"name":"@sapphire/shapeshift","weeklyDownloads":336975},{"name":"simple-runtypes","weeklyDownloads":880},{"name":"@sinclair/typebox-(ahead-of-time)","weeklyDownloads":67369474},{"name":"@sinclair/typebox-(dynamic)","weeklyDownloads":67369474},{"name":"@sinclair/typebox-(just-in-time)","weeklyDownloads":67369474},{"name":"spectypes","weeklyDownloads":232},{"name":"succulent","weeklyDownloads":135},{"name":"superstruct","weeklyDownloads":3217354},{"name":"suretype","weeklyDownloads":40167},{"name":"sury","weeklyDownloads":7975},{"name":"tiny-schema-validator","weeklyDownloads":652},{"name":"to-typed","weeklyDownloads":140},{"name":"toi","weeklyDownloads":585},{"name":"ts-interface-checker","weeklyDownloads":17874770},{"name":"ts-json-validator","weeklyDownloads":12634},{"name":"ts-runtime-checks","weeklyDownloads":551}] -------------------------------------------------------------------------------- /.github/workflows/download-packages-popularity.yml: -------------------------------------------------------------------------------- 1 | name: Download and save packages popularity 2 | 3 | env: 4 | CI: "true" 5 | 6 | on: 7 | schedule: 8 | # Runs at 00:00 UTC every Monday 9 | - cron: '0 0 * * 1' 10 | push: 11 | branches: 12 | - master 13 | paths: 14 | - .github/workflows/*.yml 15 | - "cases/*.ts" 16 | - "*.ts" 17 | - package.json 18 | - package-lock.json 19 | - bun.lock 20 | 21 | jobs: 22 | build: 23 | name: "Node ${{ matrix.node-version }}" 24 | 25 | runs-on: ubuntu-latest 26 | 27 | strategy: 28 | max-parallel: 1 29 | matrix: 30 | node-version: 31 | - 23.x 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - name: Cache node modules 37 | uses: actions/cache@v4 38 | env: 39 | cache-name: cache-node-modules 40 | with: 41 | path: ~/.npm 42 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 43 | restore-keys: | 44 | ${{ runner.os }}-build-${{ env.cache-name }}- 45 | ${{ runner.os }}-build- 46 | ${{ runner.os }}- 47 | 48 | - name: Use Node.js ${{ matrix.node-version }} 49 | uses: actions/setup-node@v4 50 | with: 51 | node-version: ${{ matrix.node-version }} 52 | 53 | - name: npm install 54 | run: npm ci 55 | 56 | - name: download-packages-popularity 57 | run: npm run download-packages-popularity 58 | 59 | - name: push 60 | uses: EndBug/add-and-commit@v9 61 | ## prevents forked repos from comitting results in PRs 62 | if: github.repository == 'moltar/typescript-runtime-type-benchmarks' 63 | with: 64 | author_name: ${{ env.GIT_COMMIT_AUTHOR_NAME }} 65 | author_email: ${{ env.GIT_COMMIT_AUTHOR_EMAIL }} 66 | message: 'feat: adds popularity of packages' 67 | push: true 68 | -------------------------------------------------------------------------------- /benchmarks/assertStrict.ts: -------------------------------------------------------------------------------- 1 | import { Benchmark } from './helpers/types'; 2 | import { validateData } from './parseSafe'; 3 | import type { ExpectStatic, SuiteAPI, TestAPI } from 'vitest'; 4 | 5 | type Fn = (data: unknown) => boolean; 6 | 7 | /** 8 | * Check that an object conforms to the schema. 9 | * 10 | * Raise errors if any extra keys not present in the schema are found. 11 | */ 12 | export class AssertStrict extends Benchmark { 13 | run() { 14 | this.fn(validateData); 15 | } 16 | 17 | test(describe: SuiteAPI, expect: ExpectStatic, test: TestAPI) { 18 | describe(this.moduleName, () => { 19 | test('should validate the data', () => { 20 | expect(this.fn(validateData)).toBe(true); 21 | }); 22 | 23 | test('should throw on unknown attributes', () => { 24 | const dataWithExtraKeys = { 25 | ...validateData, 26 | extraAttribute: 'foo', 27 | }; 28 | 29 | expect(() => this.fn(dataWithExtraKeys)).toThrow(); 30 | }); 31 | 32 | test('should throw on unknown attributes (nested)', () => { 33 | const dataWithExtraNestedKeys = { 34 | ...validateData, 35 | deeplyNested: { 36 | ...validateData.deeplyNested, 37 | extraNestedAttribute: 'bar', 38 | }, 39 | }; 40 | 41 | expect(() => this.fn(dataWithExtraNestedKeys)).toThrow(); 42 | }); 43 | 44 | test('should throw on missing attributes', () => { 45 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 46 | const data: any = { 47 | ...validateData, 48 | }; 49 | 50 | delete data.number; 51 | 52 | expect(() => this.fn(data)).toThrow(); 53 | }); 54 | 55 | test('should throw on data with an invalid attribute', () => { 56 | expect(() => 57 | this.fn({ 58 | ...validateData, 59 | number: 'foo', 60 | }), 61 | ).toThrow(); 62 | }); 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cases/bueno.ts: -------------------------------------------------------------------------------- 1 | import { 2 | boolean, 3 | check, 4 | enUS, 5 | number, 6 | object, 7 | objectExact, 8 | objectInexact, 9 | result, 10 | string, 11 | } from 'bueno'; 12 | import { type UnknownData, addCase } from '../benchmarks'; 13 | 14 | const dataType = object({ 15 | number: number, 16 | negNumber: number, 17 | maxNumber: number, 18 | string: string, 19 | longString: string, 20 | boolean: boolean, 21 | deeplyNested: object({ 22 | foo: string, 23 | num: number, 24 | bool: boolean, 25 | }), 26 | }); 27 | 28 | const dataTypeStrict = objectExact({ 29 | number: number, 30 | negNumber: number, 31 | maxNumber: number, 32 | string: string, 33 | longString: string, 34 | boolean: boolean, 35 | deeplyNested: objectExact({ 36 | foo: string, 37 | num: number, 38 | bool: boolean, 39 | }), 40 | }); 41 | 42 | const dataTypeLoose = objectInexact({ 43 | number: number, 44 | negNumber: number, 45 | maxNumber: number, 46 | string: string, 47 | longString: string, 48 | boolean: boolean, 49 | deeplyNested: objectInexact({ 50 | foo: string, 51 | num: number, 52 | bool: boolean, 53 | }), 54 | }); 55 | 56 | addCase('bueno', 'parseSafe', (data: UnknownData) => { 57 | const err = check(data, dataType, enUS); 58 | 59 | if (err) { 60 | throw new Error(err); 61 | } 62 | 63 | return result(data, dataType); 64 | }); 65 | 66 | addCase('bueno', 'parseStrict', (data: UnknownData) => { 67 | const err = check(data, dataTypeStrict, enUS); 68 | 69 | if (err) { 70 | throw new Error(err); 71 | } 72 | 73 | return result(data, dataTypeStrict); 74 | }); 75 | 76 | addCase('bueno', 'assertLoose', (data: UnknownData) => { 77 | const err = check(data, dataTypeLoose, enUS); 78 | 79 | if (err) { 80 | throw new Error(err); 81 | } 82 | 83 | return true; 84 | }); 85 | 86 | addCase('bueno', 'assertStrict', (data: UnknownData) => { 87 | const err = check(data, dataTypeStrict, enUS); 88 | 89 | if (err) { 90 | throw new Error(err); 91 | } 92 | 93 | return true; 94 | }); 95 | -------------------------------------------------------------------------------- /cases/jointz.ts: -------------------------------------------------------------------------------- 1 | import jointz from 'jointz'; 2 | import { addCase } from '../benchmarks'; 3 | 4 | const dataTypeLoose = jointz 5 | .object({ 6 | number: jointz.number(), 7 | negNumber: jointz.number(), 8 | maxNumber: jointz.number(), 9 | string: jointz.string(), 10 | longString: jointz.string(), 11 | boolean: jointz.constant(true, false), 12 | deeplyNested: jointz 13 | .object({ 14 | foo: jointz.string(), 15 | num: jointz.number(), 16 | bool: jointz.constant(true, false), 17 | }) 18 | .requiredKeys('foo', 'num', 'bool') 19 | .allowUnknownKeys(true), 20 | }) 21 | .requiredKeys([ 22 | 'number', 23 | 'boolean', 24 | 'deeplyNested', 25 | 'longString', 26 | 'maxNumber', 27 | 'negNumber', 28 | 'number', 29 | 'string', 30 | ]) 31 | .allowUnknownKeys(true); 32 | 33 | const dataTypeStrict = jointz 34 | .object({ 35 | number: jointz.number(), 36 | negNumber: jointz.number(), 37 | maxNumber: jointz.number(), 38 | string: jointz.string(), 39 | longString: jointz.string(), 40 | boolean: jointz.constant(true, false), 41 | deeplyNested: jointz 42 | .object({ 43 | foo: jointz.string(), 44 | num: jointz.number(), 45 | bool: jointz.constant(true, false), 46 | }) 47 | .requiredKeys('foo', 'num', 'bool'), 48 | }) 49 | .requiredKeys([ 50 | 'number', 51 | 'boolean', 52 | 'deeplyNested', 53 | 'longString', 54 | 'maxNumber', 55 | 'negNumber', 56 | 'number', 57 | 'string', 58 | ]); 59 | 60 | addCase('jointz', 'assertLoose', data => { 61 | const errors = dataTypeLoose.validate(data); 62 | 63 | if (errors.length) { 64 | throw errors; 65 | } 66 | 67 | return true; 68 | }); 69 | 70 | addCase('jointz', 'assertStrict', data => { 71 | const errors = dataTypeStrict.validate(data); 72 | 73 | if (errors.length) { 74 | throw errors; 75 | } 76 | 77 | return true; 78 | }); 79 | 80 | addCase('jointz', 'parseStrict', data => { 81 | if (dataTypeStrict.isValid(data)) { 82 | return data; 83 | } 84 | 85 | throw dataTypeStrict.validate(data); 86 | }); 87 | -------------------------------------------------------------------------------- /cases/sapphire-shapeshift.ts: -------------------------------------------------------------------------------- 1 | import { s } from '@sapphire/shapeshift'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('@sapphire/shapeshift', 'parseSafe', () => { 5 | const dataType = s.object({ 6 | number: s.number, 7 | negNumber: s.number, 8 | maxNumber: s.number, 9 | string: s.string, 10 | longString: s.string, 11 | boolean: s.boolean, 12 | deeplyNested: s.object({ 13 | foo: s.string, 14 | num: s.number, 15 | bool: s.boolean, 16 | }), 17 | }); 18 | 19 | return data => { 20 | return dataType.parse(data); 21 | }; 22 | }); 23 | 24 | createCase('@sapphire/shapeshift', 'parseStrict', () => { 25 | const dataType = s.object({ 26 | number: s.number, 27 | negNumber: s.number, 28 | maxNumber: s.number, 29 | string: s.string, 30 | longString: s.string, 31 | boolean: s.boolean, 32 | deeplyNested: s.object({ 33 | foo: s.string, 34 | num: s.number, 35 | bool: s.boolean, 36 | }).strict, 37 | }).strict; 38 | 39 | return data => { 40 | return dataType.parse(data); 41 | }; 42 | }); 43 | 44 | createCase('@sapphire/shapeshift', 'assertLoose', () => { 45 | const dataType = s.object({ 46 | number: s.number, 47 | negNumber: s.number, 48 | maxNumber: s.number, 49 | string: s.string, 50 | longString: s.string, 51 | boolean: s.boolean, 52 | deeplyNested: s.object({ 53 | foo: s.string, 54 | num: s.number, 55 | bool: s.boolean, 56 | }).passthrough, 57 | }).passthrough; 58 | 59 | return data => { 60 | dataType.parse(data); 61 | 62 | return true; 63 | }; 64 | }); 65 | 66 | createCase('@sapphire/shapeshift', 'assertStrict', () => { 67 | const dataType = s.object({ 68 | number: s.number, 69 | negNumber: s.number, 70 | maxNumber: s.number, 71 | string: s.string, 72 | longString: s.string, 73 | boolean: s.boolean, 74 | deeplyNested: s.object({ 75 | foo: s.string, 76 | num: s.number, 77 | bool: s.boolean, 78 | }).strict, 79 | }).strict; 80 | 81 | return data => { 82 | dataType.parse(data); 83 | 84 | return true; 85 | }; 86 | }); 87 | -------------------------------------------------------------------------------- /cases/mondrian-framework.ts: -------------------------------------------------------------------------------- 1 | import { createCase } from '../benchmarks'; 2 | import { model } from '@mondrian-framework/model'; 3 | 4 | const dataType = model.object({ 5 | number: model.number(), 6 | negNumber: model.number(), 7 | maxNumber: model.number(), 8 | string: model.string(), 9 | longString: model.string(), 10 | boolean: model.boolean(), 11 | deeplyNested: model.object({ 12 | foo: model.string(), 13 | num: model.number(), 14 | bool: model.boolean(), 15 | }), 16 | }); 17 | 18 | createCase('mondrian-framework', 'parseSafe', () => { 19 | return data => { 20 | const result = dataType.decode(data, { 21 | fieldStrictness: 'allowAdditionalFields', 22 | errorReportingStrategy: 'stopAtFirstError', 23 | typeCastingStrategy: 'expectExactTypes', 24 | }); 25 | if (result.isFailure) { 26 | throw new Error(); 27 | } 28 | return result.value; 29 | }; 30 | }); 31 | 32 | createCase('mondrian-framework', 'parseStrict', () => { 33 | return data => { 34 | const result = dataType.decode(data, { 35 | fieldStrictness: 'expectExactFields', 36 | errorReportingStrategy: 'stopAtFirstError', 37 | typeCastingStrategy: 'expectExactTypes', 38 | }); 39 | if (result.isFailure) { 40 | throw new Error(); 41 | } 42 | return result.value; 43 | }; 44 | }); 45 | 46 | createCase('mondrian-framework', 'assertLoose', () => { 47 | return data => { 48 | const result = dataType.decode(data, { 49 | fieldStrictness: 'allowAdditionalFields', 50 | errorReportingStrategy: 'stopAtFirstError', 51 | typeCastingStrategy: 'expectExactTypes', 52 | }); 53 | if (result.isFailure) { 54 | throw new Error(); 55 | } 56 | return true; 57 | }; 58 | }); 59 | 60 | createCase('mondrian-framework', 'assertStrict', () => { 61 | return data => { 62 | const result = dataType.decode(data, { 63 | fieldStrictness: 'expectExactFields', 64 | errorReportingStrategy: 'stopAtFirstError', 65 | typeCastingStrategy: 'expectExactTypes', 66 | }); 67 | if (result.isFailure) { 68 | throw new Error(); 69 | } 70 | return true; 71 | }; 72 | }); 73 | -------------------------------------------------------------------------------- /benchmarks/assertLoose.ts: -------------------------------------------------------------------------------- 1 | import { Benchmark } from './helpers/types'; 2 | import { validateData } from './parseSafe'; 3 | import type { ExpectStatic, SuiteAPI, TestAPI } from 'vitest'; 4 | 5 | type Fn = (data: unknown) => boolean; 6 | 7 | /** 8 | * Check that an object conforms to the schema. 9 | * 10 | * Ignore any extra keys in input objects. 11 | * 12 | * Such a validation mode is highly unsafe when used on untrusted input. 13 | * 14 | * But not checking for unknown/extra keys in records may provide massive 15 | * speedups and may suffice in certain scenarios. 16 | */ 17 | export class AssertLoose extends Benchmark { 18 | run() { 19 | this.fn(validateData); 20 | } 21 | 22 | test(describe: SuiteAPI, expect: ExpectStatic, test: TestAPI) { 23 | describe(this.moduleName, () => { 24 | test('should validate the data', () => { 25 | expect(this.fn(validateData)).toBe(true); 26 | }); 27 | 28 | test('should validate with unknown attributes', () => { 29 | const dataWithExtraKeys = { 30 | ...validateData, 31 | extraAttribute: 'foo', 32 | }; 33 | 34 | expect(this.fn(dataWithExtraKeys)).toBe(true); 35 | }); 36 | 37 | test('should validate with unknown attributes (nested)', () => { 38 | const dataWithExtraNestedKeys = { 39 | ...validateData, 40 | deeplyNested: { 41 | ...validateData.deeplyNested, 42 | extraNestedAttribute: 'bar', 43 | }, 44 | }; 45 | 46 | expect(this.fn(dataWithExtraNestedKeys)).toBe(true); 47 | }); 48 | 49 | test('should throw on missing attributes', () => { 50 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 51 | const data: any = { 52 | ...validateData, 53 | }; 54 | 55 | delete data.number; 56 | 57 | expect(() => this.fn(data)).toThrow(); 58 | }); 59 | 60 | test('should throw on data with an invalid attribute', () => { 61 | expect(() => 62 | this.fn({ 63 | ...validateData, 64 | number: 'foo', 65 | }), 66 | ).toThrow(); 67 | }); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /cases/zod4.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod4'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('zod4', 'parseSafe', () => { 5 | const dataType = z.interface({ 6 | number: z.number(), 7 | negNumber: z.number(), 8 | maxNumber: z.number(), 9 | string: z.string(), 10 | longString: z.string(), 11 | boolean: z.boolean(), 12 | deeplyNested: z.interface({ 13 | foo: z.string(), 14 | num: z.number(), 15 | bool: z.boolean(), 16 | }), 17 | }); 18 | 19 | return data => { 20 | return dataType.parse(data); 21 | }; 22 | }); 23 | 24 | createCase('zod4', 'parseStrict', () => { 25 | const dataType = z 26 | .interface({ 27 | number: z.number(), 28 | negNumber: z.number(), 29 | maxNumber: z.number(), 30 | string: z.string(), 31 | longString: z.string(), 32 | boolean: z.boolean(), 33 | deeplyNested: z 34 | .interface({ 35 | foo: z.string(), 36 | num: z.number(), 37 | bool: z.boolean(), 38 | }) 39 | .strict(), 40 | }) 41 | .strict(); 42 | 43 | return data => { 44 | return dataType.parse(data); 45 | }; 46 | }); 47 | 48 | createCase('zod4', 'assertLoose', () => { 49 | const dataType = z.looseInterface({ 50 | number: z.number(), 51 | negNumber: z.number(), 52 | maxNumber: z.number(), 53 | string: z.string(), 54 | longString: z.string(), 55 | boolean: z.boolean(), 56 | deeplyNested: z.looseInterface({ 57 | foo: z.string(), 58 | num: z.number(), 59 | bool: z.boolean(), 60 | }), 61 | }); 62 | 63 | return data => { 64 | dataType.parse(data); 65 | 66 | return true; 67 | }; 68 | }); 69 | 70 | createCase('zod4', 'assertStrict', () => { 71 | const dataType = z.strictInterface({ 72 | number: z.number(), 73 | negNumber: z.number(), 74 | maxNumber: z.number(), 75 | string: z.string(), 76 | longString: z.string(), 77 | boolean: z.boolean(), 78 | deeplyNested: z.strictInterface({ 79 | foo: z.string(), 80 | num: z.number(), 81 | bool: z.boolean(), 82 | }), 83 | }); 84 | 85 | return data => { 86 | dataType.parse(data); 87 | 88 | return true; 89 | }; 90 | }); 91 | -------------------------------------------------------------------------------- /cases/deepkit/build/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.assertLoose = assertLoose; 4 | exports.assertStrict = assertStrict; 5 | exports.parseStrict = parseStrict; 6 | exports.parseSafe = parseSafe; 7 | const type_1 = require("@deepkit/type"); 8 | const __ΩToBeChecked = ['number', 'negNumber', 'maxNumber', 'string', 'longString', 'boolean', 'foo', 'num', 'bool', 'deeplyNested', 'ToBeChecked', 'P\'4!\'4"\'4#&4$&4%)4&P&4\'\'4()4)M4*Mw+y']; 9 | const isToBeChecked = (type_1.getValidatorFunction.Ω = [[() => __ΩToBeChecked, 'n!']], (0, type_1.getValidatorFunction)()); 10 | const safeToBeChecked = (type_1.castFunction.Ω = [[() => __ΩToBeChecked, 'n!']], (0, type_1.castFunction)()); 11 | /** 12 | * Check that an object conforms to the schema. 13 | * 14 | * Ignore any extra keys in input objects. 15 | * 16 | * Such a validation mode is highly unsafe when used on untrusted input. 17 | * 18 | * But not checking for unknown/extra keys in records may provide massive 19 | * speedups and may suffice in certain scenarios. 20 | */ 21 | function assertLoose(input) { 22 | if (!isToBeChecked(input)) 23 | throw new Error('wrong type.'); 24 | return true; 25 | } 26 | assertLoose.__type = ['input', 'assertLoose', 'P#2!)/"']; 27 | /** 28 | * Check that an object conforms to the schema. 29 | * 30 | * Raise errors if any extra keys not present in the schema are found. 31 | */ 32 | function assertStrict() { 33 | throw new Error('not supported.'); 34 | } 35 | assertStrict.__type = ['assertStrict', 'P)/!']; 36 | /** 37 | * Like parseSafe but throw on unknown (extra) keys in objects. 38 | */ 39 | function parseStrict() { 40 | throw new Error('not supported.'); 41 | } 42 | parseStrict.__type = [() => __ΩToBeChecked, 'parseStrict', 'Pn!/"']; 43 | /** 44 | * Validate and ignore unknown keys, removing them from the result. 45 | * 46 | * When validating untrusted data, unknown keys should always be removed to 47 | * not result in unwanted parameters or the `__proto__` attribute being 48 | * maliciously passed to internal functions. 49 | */ 50 | function parseSafe(input) { 51 | return safeToBeChecked(input); 52 | } 53 | parseSafe.__type = ['input', () => __ΩToBeChecked, 'parseSafe', 'P#2!n"/#']; 54 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /cases/jet-validators.ts: -------------------------------------------------------------------------------- 1 | import { isString, isNumber, isBoolean } from 'jet-validators'; 2 | 3 | import { 4 | parseObject, 5 | looseTestObject, 6 | strictParseObject, 7 | strictTestObject, 8 | } from 'jet-validators/utils'; 9 | 10 | import { createCase } from '../benchmarks'; 11 | 12 | // **** Init Schema **** // 13 | 14 | const safeParse = parseObject({ 15 | number: isNumber, 16 | negNumber: isNumber, 17 | maxNumber: isNumber, 18 | string: isString, 19 | longString: isString, 20 | boolean: isBoolean, 21 | deeplyNested: { 22 | foo: isString, 23 | num: isNumber, 24 | bool: isBoolean, 25 | }, 26 | }); 27 | 28 | const looseTest = looseTestObject({ 29 | number: isNumber, 30 | negNumber: isNumber, 31 | maxNumber: isNumber, 32 | string: isString, 33 | longString: isString, 34 | boolean: isBoolean, 35 | deeplyNested: { 36 | foo: isString, 37 | num: isNumber, 38 | bool: isBoolean, 39 | }, 40 | }); 41 | 42 | const strictParse = strictParseObject({ 43 | number: isNumber, 44 | negNumber: isNumber, 45 | maxNumber: isNumber, 46 | string: isString, 47 | longString: isString, 48 | boolean: isBoolean, 49 | deeplyNested: { 50 | foo: isString, 51 | num: isNumber, 52 | bool: isBoolean, 53 | }, 54 | }); 55 | 56 | const strictTest = strictTestObject({ 57 | number: isNumber, 58 | negNumber: isNumber, 59 | maxNumber: isNumber, 60 | string: isString, 61 | longString: isString, 62 | boolean: isBoolean, 63 | deeplyNested: { 64 | foo: isString, 65 | num: isNumber, 66 | bool: isBoolean, 67 | }, 68 | }); 69 | 70 | const checkFailed = (arg: unknown) => { 71 | if (arg === false) { 72 | throw new Error('Validation failed'); 73 | } else { 74 | return arg; 75 | } 76 | }; 77 | 78 | // **** Run Tests **** // 79 | 80 | // Parse "safe" 81 | createCase('jet-validators', 'parseSafe', () => { 82 | return data => checkFailed(safeParse(data)); 83 | }); 84 | 85 | // Parse "strict" 86 | createCase('jet-validators', 'parseStrict', () => { 87 | return data => checkFailed(strictParse(data)); 88 | }); 89 | 90 | // Test "loose" 91 | createCase('jet-validators', 'assertLoose', () => { 92 | return data => checkFailed(looseTest(data)); 93 | }); 94 | 95 | // Test "strict" 96 | createCase('jet-validators', 'assertStrict', () => { 97 | return data => checkFailed(strictTest(data)); 98 | }); 99 | -------------------------------------------------------------------------------- /cases/r-assign.ts: -------------------------------------------------------------------------------- 1 | import { 2 | boolean, 3 | number, 4 | object, 5 | strictObject, 6 | string, 7 | parseType, 8 | } from 'r-assign/lib'; 9 | import { createCase } from '../benchmarks'; 10 | 11 | createCase('r-assign', 'parseSafe', () => { 12 | const dataType = object({ 13 | number: number, 14 | negNumber: number, 15 | maxNumber: number, 16 | string: string, 17 | longString: string, 18 | boolean: boolean, 19 | deeplyNested: object({ 20 | foo: string, 21 | num: number, 22 | bool: boolean, 23 | }), 24 | }); 25 | 26 | const parseData = parseType(dataType); 27 | 28 | return data => { 29 | return parseData(data); 30 | }; 31 | }); 32 | 33 | createCase('r-assign', 'parseStrict', () => { 34 | const dataType = strictObject({ 35 | number: number, 36 | negNumber: number, 37 | maxNumber: number, 38 | string: string, 39 | longString: string, 40 | boolean: boolean, 41 | deeplyNested: strictObject({ 42 | foo: string, 43 | num: number, 44 | bool: boolean, 45 | }), 46 | }); 47 | 48 | const parseData = parseType(dataType); 49 | 50 | return data => { 51 | return parseData(data); 52 | }; 53 | }); 54 | 55 | createCase('r-assign', 'assertLoose', () => { 56 | const dataType = object({ 57 | number: number, 58 | negNumber: number, 59 | maxNumber: number, 60 | string: string, 61 | longString: string, 62 | boolean: boolean, 63 | deeplyNested: object({ 64 | foo: string, 65 | num: number, 66 | bool: boolean, 67 | }), 68 | }); 69 | 70 | const parseData = parseType(dataType); 71 | 72 | return data => { 73 | parseData(data); 74 | 75 | return true; 76 | }; 77 | }); 78 | 79 | createCase('r-assign', 'assertStrict', () => { 80 | const dataType = strictObject({ 81 | number: number, 82 | negNumber: number, 83 | maxNumber: number, 84 | string: string, 85 | longString: string, 86 | boolean: boolean, 87 | deeplyNested: strictObject({ 88 | foo: string, 89 | num: number, 90 | bool: boolean, 91 | }), 92 | }); 93 | 94 | const parseData = parseType(dataType); 95 | 96 | return data => { 97 | parseData(data); 98 | 99 | return true; 100 | }; 101 | }); 102 | -------------------------------------------------------------------------------- /cases/zod.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('zod', 'parseSafe', () => { 5 | const dataType = z.object({ 6 | number: z.number(), 7 | negNumber: z.number(), 8 | maxNumber: z.number(), 9 | string: z.string(), 10 | longString: z.string(), 11 | boolean: z.boolean(), 12 | deeplyNested: z.object({ 13 | foo: z.string(), 14 | num: z.number(), 15 | bool: z.boolean(), 16 | }), 17 | }); 18 | 19 | return data => { 20 | return dataType.parse(data); 21 | }; 22 | }); 23 | 24 | createCase('zod', 'parseStrict', () => { 25 | const dataType = z 26 | .object({ 27 | number: z.number(), 28 | negNumber: z.number(), 29 | maxNumber: z.number(), 30 | string: z.string(), 31 | longString: z.string(), 32 | boolean: z.boolean(), 33 | deeplyNested: z 34 | .object({ 35 | foo: z.string(), 36 | num: z.number(), 37 | bool: z.boolean(), 38 | }) 39 | .strict(), 40 | }) 41 | .strict(); 42 | 43 | return data => { 44 | return dataType.parse(data); 45 | }; 46 | }); 47 | 48 | createCase('zod', 'assertLoose', () => { 49 | const dataType = z 50 | .object({ 51 | number: z.number(), 52 | negNumber: z.number(), 53 | maxNumber: z.number(), 54 | string: z.string(), 55 | longString: z.string(), 56 | boolean: z.boolean(), 57 | deeplyNested: z 58 | .object({ 59 | foo: z.string(), 60 | num: z.number(), 61 | bool: z.boolean(), 62 | }) 63 | .passthrough(), 64 | }) 65 | .passthrough(); 66 | 67 | return data => { 68 | dataType.parse(data); 69 | 70 | return true; 71 | }; 72 | }); 73 | 74 | createCase('zod', 'assertStrict', () => { 75 | const dataType = z 76 | .object({ 77 | number: z.number(), 78 | negNumber: z.number(), 79 | maxNumber: z.number(), 80 | string: z.string(), 81 | longString: z.string(), 82 | boolean: z.boolean(), 83 | deeplyNested: z 84 | .object({ 85 | foo: z.string(), 86 | num: z.number(), 87 | bool: z.boolean(), 88 | }) 89 | .strict(), 90 | }) 91 | .strict(); 92 | 93 | return data => { 94 | dataType.parse(data); 95 | 96 | return true; 97 | }; 98 | }); 99 | -------------------------------------------------------------------------------- /cases/tson.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@skarab/tson'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('tson', 'parseSafe', () => { 5 | const dataType = t 6 | .object({ 7 | number: t.number(), 8 | negNumber: t.number(), 9 | maxNumber: t.number(), 10 | string: t.string(), 11 | longString: t.string(), 12 | boolean: t.boolean(), 13 | deeplyNested: t 14 | .object({ 15 | foo: t.string(), 16 | num: t.number(), 17 | bool: t.boolean(), 18 | }) 19 | .strip(), 20 | }) 21 | .strip(); 22 | 23 | return data => { 24 | return dataType.parse(data); 25 | }; 26 | }); 27 | 28 | createCase('tson', 'parseStrict', () => { 29 | const dataType = t 30 | .object({ 31 | number: t.number(), 32 | negNumber: t.number(), 33 | maxNumber: t.number(), 34 | string: t.string(), 35 | longString: t.string(), 36 | boolean: t.boolean(), 37 | deeplyNested: t 38 | .object({ 39 | foo: t.string(), 40 | num: t.number(), 41 | bool: t.boolean(), 42 | }) 43 | .strict(), 44 | }) 45 | .strict(); 46 | 47 | return data => { 48 | return dataType.parse(data); 49 | }; 50 | }); 51 | 52 | createCase('tson', 'assertLoose', () => { 53 | const dataType = t 54 | .object({ 55 | number: t.number(), 56 | negNumber: t.number(), 57 | maxNumber: t.number(), 58 | string: t.string(), 59 | longString: t.string(), 60 | boolean: t.boolean(), 61 | deeplyNested: t 62 | .object({ 63 | foo: t.string(), 64 | num: t.number(), 65 | bool: t.boolean(), 66 | }) 67 | .passthrough(), 68 | }) 69 | .passthrough(); 70 | 71 | return data => { 72 | dataType.parse(data); 73 | 74 | return true; 75 | }; 76 | }); 77 | 78 | createCase('tson', 'assertStrict', () => { 79 | const dataType = t 80 | .object({ 81 | number: t.number(), 82 | negNumber: t.number(), 83 | maxNumber: t.number(), 84 | string: t.string(), 85 | longString: t.string(), 86 | boolean: t.boolean(), 87 | deeplyNested: t 88 | .object({ 89 | foo: t.string(), 90 | num: t.number(), 91 | bool: t.boolean(), 92 | }) 93 | .strict(), 94 | }) 95 | .strict(); 96 | 97 | return data => { 98 | dataType.parse(data); 99 | 100 | return true; 101 | }; 102 | }); 103 | -------------------------------------------------------------------------------- /benchmarks/helpers/register.ts: -------------------------------------------------------------------------------- 1 | import { AssertLoose } from '../assertLoose'; 2 | import { AssertStrict } from '../assertStrict'; 3 | import { ParseSafe } from '../parseSafe'; 4 | import { ParseStrict } from '../parseStrict'; 5 | import type { BenchmarkCase } from './types'; 6 | 7 | /** 8 | * Map of all benchmarks. 9 | */ 10 | export const availableBenchmarks = { 11 | parseSafe: ParseSafe, 12 | parseStrict: ParseStrict, 13 | assertLoose: AssertLoose, 14 | assertStrict: AssertStrict, 15 | }; 16 | 17 | type AvailableBenchmarks = typeof availableBenchmarks; 18 | export type AvailableBenchmarksIds = keyof AvailableBenchmarks; 19 | 20 | const registeredBenchmarks = new Map(); 21 | 22 | /** 23 | * Return the list of all registered benchmarks. 24 | */ 25 | export function getRegisteredBenchmarks(): [ 26 | keyof AvailableBenchmarks, 27 | BenchmarkCase[], 28 | ][] { 29 | return [...registeredBenchmarks.entries()]; 30 | } 31 | 32 | /** 33 | * Add a specific benchmark implementation for a given library. 34 | */ 35 | export function addCase< 36 | K extends keyof AvailableBenchmarks, 37 | I = AvailableBenchmarks[K]['prototype']['fn'], 38 | >( 39 | moduleName: string, 40 | benchmarkId: K, 41 | implementation: I, 42 | options?: { disabled?: boolean }, 43 | ) { 44 | let benchmarks = registeredBenchmarks.get(benchmarkId); 45 | 46 | if (!benchmarks) { 47 | benchmarks = []; 48 | 49 | registeredBenchmarks.set(benchmarkId, benchmarks); 50 | } 51 | 52 | if (benchmarks.find(c => c.moduleName === moduleName)) { 53 | console.error( 54 | 'benchmark', 55 | benchmarkId, 56 | 'is already defined for module', 57 | moduleName, 58 | ); 59 | } 60 | 61 | if (options?.disabled) { 62 | return; 63 | } 64 | 65 | const benchmarkCtor = availableBenchmarks[benchmarkId]; 66 | 67 | benchmarks.push( 68 | new benchmarkCtor( 69 | moduleName, 70 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 71 | implementation as any, 72 | ), 73 | ); 74 | } 75 | 76 | export function createCase< 77 | K extends keyof AvailableBenchmarks, 78 | I = AvailableBenchmarks[K]['prototype']['fn'], 79 | >( 80 | moduleName: string, 81 | benchmarkId: K, 82 | builder: () => I, 83 | options?: { disabled?: boolean }, 84 | ) { 85 | const impl = builder(); 86 | 87 | if (!impl) { 88 | throw new Error( 89 | `case implementation function missing in benchmark "${benchmarkId}" for module "${moduleName}"`, 90 | ); 91 | } 92 | 93 | addCase(moduleName, benchmarkId, impl, options); 94 | } 95 | -------------------------------------------------------------------------------- /cases/dhi.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error - dhi uses package.json exports which require moduleResolution: "bundler" or "node16" 2 | import { z } from 'dhi/schema'; 3 | import { createCase } from '../benchmarks'; 4 | 5 | createCase('dhi', 'parseSafe', () => { 6 | const dataType = z.object({ 7 | number: z.number(), 8 | negNumber: z.number(), 9 | maxNumber: z.number(), 10 | string: z.string(), 11 | longString: z.string(), 12 | boolean: z.boolean(), 13 | deeplyNested: z.object({ 14 | foo: z.string(), 15 | num: z.number(), 16 | bool: z.boolean(), 17 | }), 18 | }); // Default behavior: strips unknown keys 19 | 20 | return data => { 21 | return dataType.parse(data); 22 | }; 23 | }); 24 | 25 | createCase('dhi', 'parseStrict', () => { 26 | const dataType = z 27 | .object({ 28 | number: z.number(), 29 | negNumber: z.number(), 30 | maxNumber: z.number(), 31 | string: z.string(), 32 | longString: z.string(), 33 | boolean: z.boolean(), 34 | deeplyNested: z 35 | .object({ 36 | foo: z.string(), 37 | num: z.number(), 38 | bool: z.boolean(), 39 | }) 40 | .strict(), // Throw on unknown keys (nested) 41 | }) 42 | .strict(); // Throw on unknown keys (root) 43 | 44 | return data => { 45 | return dataType.parse(data); 46 | }; 47 | }); 48 | 49 | createCase('dhi', 'assertLoose', () => { 50 | const dataType = z 51 | .object({ 52 | number: z.number(), 53 | negNumber: z.number(), 54 | maxNumber: z.number(), 55 | string: z.string(), 56 | longString: z.string(), 57 | boolean: z.boolean(), 58 | deeplyNested: z 59 | .object({ 60 | foo: z.string(), 61 | num: z.number(), 62 | bool: z.boolean(), 63 | }) 64 | .passthrough(), // Allow unknown keys (nested) 65 | }) 66 | .passthrough(); // Allow unknown keys (root) 67 | 68 | return data => { 69 | dataType.parse(data); 70 | return true; 71 | }; 72 | }); 73 | 74 | createCase('dhi', 'assertStrict', () => { 75 | const dataType = z 76 | .object({ 77 | number: z.number(), 78 | negNumber: z.number(), 79 | maxNumber: z.number(), 80 | string: z.string(), 81 | longString: z.string(), 82 | boolean: z.boolean(), 83 | deeplyNested: z 84 | .object({ 85 | foo: z.string(), 86 | num: z.number(), 87 | bool: z.boolean(), 88 | }) 89 | .strict(), // Throw on unknown keys (nested) 90 | }) 91 | .strict(); // Throw on unknown keys (root) 92 | 93 | return data => { 94 | dataType.parse(data); 95 | return true; 96 | }; 97 | }); 98 | -------------------------------------------------------------------------------- /cases/effect-schema.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '@effect/schema'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('effect-schema', 'parseSafe', () => { 5 | const dataType = Schema.Struct({ 6 | number: Schema.Number, 7 | negNumber: Schema.Number, 8 | maxNumber: Schema.Number, 9 | string: Schema.String, 10 | longString: Schema.String, 11 | boolean: Schema.Boolean, 12 | deeplyNested: Schema.Struct({ 13 | foo: Schema.String, 14 | num: Schema.Number, 15 | bool: Schema.Boolean, 16 | }), 17 | }); 18 | 19 | const parse = Schema.decodeUnknownSync(dataType, { 20 | onExcessProperty: undefined, 21 | }); 22 | 23 | return data => { 24 | return parse(data); 25 | }; 26 | }); 27 | 28 | createCase('effect-schema', 'parseStrict', () => { 29 | const dataType = Schema.Struct({ 30 | number: Schema.Number, 31 | negNumber: Schema.Number, 32 | maxNumber: Schema.Number, 33 | string: Schema.String, 34 | longString: Schema.String, 35 | boolean: Schema.Boolean, 36 | deeplyNested: Schema.Struct({ 37 | foo: Schema.String, 38 | num: Schema.Number, 39 | bool: Schema.Boolean, 40 | }), 41 | }); 42 | 43 | const parse = Schema.decodeUnknownSync(dataType, { 44 | onExcessProperty: 'error', 45 | }); 46 | 47 | return data => { 48 | return parse(data); 49 | }; 50 | }); 51 | 52 | createCase('effect-schema', 'assertLoose', () => { 53 | const dataType = Schema.Struct({ 54 | number: Schema.Number, 55 | negNumber: Schema.Number, 56 | maxNumber: Schema.Number, 57 | string: Schema.String, 58 | longString: Schema.String, 59 | boolean: Schema.Boolean, 60 | deeplyNested: Schema.Struct({ 61 | foo: Schema.String, 62 | num: Schema.Number, 63 | bool: Schema.Boolean, 64 | }), 65 | }); 66 | 67 | const asserts = Schema.asserts(dataType, { 68 | onExcessProperty: 'ignore', 69 | }); 70 | 71 | return data => { 72 | asserts(data)!; 73 | return true; 74 | }; 75 | }); 76 | 77 | createCase('effect-schema', 'assertStrict', () => { 78 | const dataType = Schema.Struct({ 79 | number: Schema.Number, 80 | negNumber: Schema.Number, 81 | maxNumber: Schema.Number, 82 | string: Schema.String, 83 | longString: Schema.String, 84 | boolean: Schema.Boolean, 85 | deeplyNested: Schema.Struct({ 86 | foo: Schema.String, 87 | num: Schema.Number, 88 | bool: Schema.Boolean, 89 | }), 90 | }); 91 | 92 | const validate = Schema.asserts(dataType, { 93 | onExcessProperty: 'error', 94 | }); 95 | 96 | return data => { 97 | validate(data)!; 98 | return true; 99 | }; 100 | }); 101 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Pull Requests 2 | 3 | env: 4 | CI: 'true' 5 | 6 | on: 7 | - pull_request 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | max-parallel: 3 15 | matrix: 16 | node-version: 17 | - 20.x 18 | - 21.x 19 | - 22.x 20 | - 23.x 21 | - 24.x 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v4 28 | env: 29 | cache-name: cache-node-modules 30 | with: 31 | path: ~/.npm 32 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 33 | restore-keys: | 34 | ${{ runner.os }}-build-${{ env.cache-name }}- 35 | ${{ runner.os }}-build- 36 | ${{ runner.os }}- 37 | 38 | - name: Use Node.js ${{ matrix.node-version }} 39 | uses: actions/setup-node@v4 40 | with: 41 | node-version: ${{ matrix.node-version }} 42 | 43 | - name: install 44 | run: npm ci 45 | 46 | - name: lint 47 | run: npm run lint 48 | 49 | - name: test build 50 | run: npm run test:build 51 | 52 | - name: test 53 | run: npm test 54 | build-bun: 55 | needs: build 56 | runs-on: ubuntu-latest 57 | 58 | strategy: 59 | max-parallel: 3 60 | matrix: 61 | bun-version: 62 | - 1.2.12 63 | 64 | steps: 65 | - uses: actions/checkout@v4 66 | 67 | - name: Set up Bun ${{ matrix.bun-version }} 68 | uses: oven-sh/setup-bun@v2 69 | with: 70 | bun-version: ${{ matrix.bun-version }} 71 | 72 | - name: install 73 | run: bun install 74 | 75 | - name: lint 76 | run: bun run lint 77 | 78 | - name: test build 79 | run: bun run test:build 80 | 81 | - name: test 82 | run: bun test 83 | build-deno: 84 | needs: build 85 | runs-on: ubuntu-latest 86 | 87 | strategy: 88 | max-parallel: 3 89 | matrix: 90 | deno-version: 91 | - 2.1.9 92 | 93 | steps: 94 | - uses: actions/checkout@v4 95 | 96 | - uses: actions/setup-node@v4 97 | 98 | - name: Set up Deno ${{ matrix.deno-version }} 99 | uses: denoland/setup-deno@v2 100 | with: 101 | deno-version: ${{ matrix.deno-version }} 102 | 103 | - name: Install 104 | run: npm ci 105 | 106 | - name: lint 107 | run: deno run lint 108 | 109 | - name: test build 110 | run: deno task test:build 111 | 112 | - name: test 113 | run: deno task test 114 | -------------------------------------------------------------------------------- /test/benchmarks.test.ts: -------------------------------------------------------------------------------- 1 | import { test, describe, expect } from 'vitest'; 2 | import { getRegisteredBenchmarks } from '../benchmarks'; 3 | import { cases } from '../cases'; 4 | 5 | // all cases need to be imported here because vitest cannot pick up dynamically 6 | // imported `test` and `describe` 7 | import '../cases/aeria'; 8 | import '../cases/ajv'; 9 | import '../cases/arktype'; 10 | import '../cases/banditypes'; 11 | import '../cases/bueno'; 12 | import '../cases/caketype'; 13 | import '../cases/class-validator'; 14 | import '../cases/cleaners'; 15 | import '../cases/computed-types'; 16 | import '../cases/decoders'; 17 | import '../cases/io-ts'; 18 | import '../cases/joi'; 19 | import '../cases/jointz'; 20 | import '../cases/json-decoder'; 21 | import '../cases/mol_data'; 22 | import '../cases/mojotech-json-type-validation'; 23 | import '../cases/mondrian-framework'; 24 | import '../cases/myzod'; 25 | import '../cases/ok-computer'; 26 | import '../cases/parse-dont-validate'; 27 | import '../cases/paseri'; 28 | import '../cases/pure-parse'; 29 | import '../cases/purify-ts'; 30 | import '../cases/r-assign'; 31 | import '../cases/rescript-schema'; 32 | import '../cases/rulr'; 33 | import '../cases/runtypes'; 34 | import '../cases/sapphire-shapeshift'; 35 | import '../cases/simple-runtypes'; 36 | import '../cases/sinclair-typebox-ahead-of-time'; 37 | import '../cases/sinclair-typebox-dynamic'; 38 | import '../cases/sinclair-typebox-just-in-time'; 39 | import '../cases/sinclair-typemap-valibot'; 40 | import '../cases/sinclair-typemap-zod'; 41 | import '../cases/spectypes'; 42 | import '../cases/stnl'; 43 | import '../cases/succulent'; 44 | import '../cases/superstruct'; 45 | import '../cases/suretype'; 46 | import '../cases/sury'; 47 | import '../cases/to-typed'; 48 | import '../cases/toi'; 49 | import '../cases/ts-interface-checker'; 50 | import '../cases/ts-json-validator'; 51 | import '../cases/ts-runtime-checks'; 52 | import '../cases/ts-utils'; 53 | import '../cases/tson'; 54 | import '../cases/typeofweb-schema'; 55 | import '../cases/typia'; 56 | import '../cases/unknownutil'; 57 | import '../cases/valibot'; 58 | import '../cases/valita'; 59 | import '../cases/vality'; 60 | import '../cases/yup'; 61 | import '../cases/zod'; 62 | import '../cases/zod4'; 63 | import '../cases/deepkit'; 64 | import '../cases/effect-schema'; 65 | import '../cases/ts-auto-guard'; 66 | import '../cases/type-predicate-generator'; 67 | import '../cases/tiny-schema-validator'; 68 | import '../cases/jet-validators'; 69 | 70 | test('all cases must have been imported in tests', () => { 71 | expect( 72 | new Set( 73 | getRegisteredBenchmarks().flatMap(pair => 74 | pair[1].map(b => b.moduleName.split(' ')[0]), 75 | ), 76 | ).size, 77 | ).toBe(cases.length); 78 | }); 79 | 80 | getRegisteredBenchmarks().forEach(([benchmarkId, benchmarkCases]) => { 81 | describe(benchmarkId, () => { 82 | benchmarkCases.forEach(c => c.test(describe, expect, test)); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /cases/ajv.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv'; 2 | import { createCase } from '../benchmarks'; 3 | 4 | createCase('ajv', 'assertLoose', () => { 5 | const ajv = new Ajv(); 6 | const schema = { 7 | $id: 'AjvTest', 8 | $schema: 'http://json-schema.org/draft-07/schema#', 9 | type: 'object', 10 | properties: { 11 | number: { 12 | type: 'number', 13 | }, 14 | negNumber: { 15 | type: 'number', 16 | }, 17 | maxNumber: { 18 | type: 'number', 19 | }, 20 | string: { 21 | type: 'string', 22 | }, 23 | longString: { 24 | type: 'string', 25 | }, 26 | boolean: { 27 | type: 'boolean', 28 | }, 29 | deeplyNested: { 30 | type: 'object', 31 | properties: { 32 | foo: { 33 | type: 'string', 34 | }, 35 | num: { 36 | type: 'number', 37 | }, 38 | bool: { 39 | type: 'boolean', 40 | }, 41 | }, 42 | required: ['foo', 'num', 'bool'], 43 | }, 44 | }, 45 | required: [ 46 | 'number', 47 | 'negNumber', 48 | 'maxNumber', 49 | 'string', 50 | 'longString', 51 | 'boolean', 52 | 'deeplyNested', 53 | ], 54 | }; 55 | const validate = ajv.compile(schema); 56 | 57 | return data => { 58 | if (!validate(data)) { 59 | throw new Error(JSON.stringify(ajv.errors, null, 4)); 60 | } 61 | 62 | return true; 63 | }; 64 | }); 65 | 66 | createCase('ajv', 'assertStrict', () => { 67 | const ajv = new Ajv(); 68 | const schema = { 69 | $id: 'AjvTest', 70 | $schema: 'http://json-schema.org/draft-07/schema#', 71 | type: 'object', 72 | properties: { 73 | number: { 74 | type: 'number', 75 | }, 76 | negNumber: { 77 | type: 'number', 78 | }, 79 | maxNumber: { 80 | type: 'number', 81 | }, 82 | string: { 83 | type: 'string', 84 | }, 85 | longString: { 86 | type: 'string', 87 | }, 88 | boolean: { 89 | type: 'boolean', 90 | }, 91 | deeplyNested: { 92 | type: 'object', 93 | properties: { 94 | foo: { 95 | type: 'string', 96 | }, 97 | num: { 98 | type: 'number', 99 | }, 100 | bool: { 101 | type: 'boolean', 102 | }, 103 | }, 104 | required: ['foo', 'num', 'bool'], 105 | additionalProperties: false, 106 | }, 107 | }, 108 | required: [ 109 | 'number', 110 | 'negNumber', 111 | 'maxNumber', 112 | 'string', 113 | 'longString', 114 | 'boolean', 115 | 'deeplyNested', 116 | ], 117 | additionalProperties: false, 118 | }; 119 | const validate = ajv.compile(schema); 120 | 121 | return data => { 122 | if (!validate(data)) { 123 | throw new Error(JSON.stringify(ajv.errors, null, 4)); 124 | } 125 | 126 | return true; 127 | }; 128 | }); 129 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # Snowpack dependency directory (https://snowpack.dev/) 53 | web_modules/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Optional stylelint cache 65 | .stylelintcache 66 | 67 | # Microbundle cache 68 | .rpt2_cache/ 69 | .rts2_cache_cjs/ 70 | .rts2_cache_es/ 71 | .rts2_cache_umd/ 72 | 73 | # Optional REPL history 74 | .node_repl_history 75 | 76 | # Output of 'npm pack' 77 | *.tgz 78 | 79 | # Yarn Integrity file 80 | .yarn-integrity 81 | 82 | # dotenv environment variable files 83 | .env 84 | .env.development.local 85 | .env.test.local 86 | .env.production.local 87 | .env.local 88 | .env.test 89 | 90 | # parcel-bundler cache (https://parceljs.org/) 91 | .cache 92 | .parcel-cache 93 | 94 | # Next.js build output 95 | .next 96 | out 97 | 98 | # Nuxt.js build / generate output 99 | .nuxt 100 | dist 101 | 102 | # Gatsby files 103 | .cache/ 104 | # Comment in the public line in if your project uses Gatsby and not Next.js 105 | # https://nextjs.org/blog/next-9-1#public-directory-support 106 | # public 107 | 108 | # vuepress build output 109 | .vuepress/dist 110 | 111 | # vuepress v2.x temp and cache directory 112 | .temp 113 | 114 | # Docusaurus cache and generated files 115 | .docusaurus 116 | 117 | # Serverless directories 118 | .serverless/ 119 | 120 | # FuseBox cache 121 | .fusebox/ 122 | 123 | # DynamoDB Local files 124 | .dynamodb/ 125 | 126 | # TernJS port file 127 | .tern-port 128 | 129 | # Stores VSCode versions used for testing VSCode extensions 130 | .vscode-test 131 | 132 | # yarn v2 133 | .yarn/cache 134 | .yarn/unplugged 135 | .yarn/build-state.yml 136 | .yarn/install-state.gz 137 | .pnp.* 138 | 139 | ### Node Patch ### 140 | # Serverless Webpack directories 141 | .webpack/ 142 | 143 | # Optional stylelint cache 144 | 145 | # SvelteKit build / generate output 146 | .svelte-kit 147 | 148 | # End of https://www.toptal.com/developers/gitignore/api/node 149 | 150 | # spectype build artifacts 151 | cases/spectypes/build 152 | 153 | # ts-runtime-checks build artifacts 154 | cases/ts-runtime-checks/build 155 | 156 | # typia build artifacts 157 | cases/typia/build 158 | 159 | # ts-auto-guard build artifacts 160 | cases/ts-auto-guard/build 161 | cases/ts-auto-guard/src/index.guard.ts 162 | 163 | # type-predicate-generator build artifacts 164 | cases/type-predicate-generator/build 165 | cases/type-predicate-generator/src/index_guards.ts 166 | 167 | # Paseri 168 | cases/paseri/build 169 | 170 | # Bun binary lock file makes mergin harder. Using text lock file instead. 171 | bun.lockb 172 | -------------------------------------------------------------------------------- /benchmarks/parseSafe.ts: -------------------------------------------------------------------------------- 1 | import { Benchmark } from './helpers/types'; 2 | import type { ExpectStatic, SuiteAPI, TestAPI } from 'vitest'; 3 | 4 | export const validateData = Object.freeze({ 5 | number: 1, 6 | negNumber: -1, 7 | maxNumber: Number.MAX_VALUE, 8 | string: 'string', 9 | longString: 10 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Vivendum intellegat et qui, ei denique consequuntur vix. Semper aeterno percipit ut his, sea ex utinam referrentur repudiandae. No epicuri hendrerit consetetur sit, sit dicta adipiscing ex, in facete detracto deterruisset duo. Quot populo ad qui. Sit fugit nostrum et. Ad per diam dicant interesset, lorem iusto sensibus ut sed. No dicam aperiam vis. Pri posse graeco definitiones cu, id eam populo quaestio adipiscing, usu quod malorum te. Ex nam agam veri, dicunt efficiantur ad qui, ad legere adversarium sit. Commune platonem mel id, brute adipiscing duo an. Vivendum intellegat et qui, ei denique consequuntur vix. Offendit eleifend moderatius ex vix, quem odio mazim et qui, purto expetendis cotidieque quo cu, veri persius vituperata ei nec. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.', 11 | boolean: true, 12 | deeplyNested: { 13 | foo: 'bar', 14 | num: 1, 15 | bool: false, 16 | }, 17 | }); 18 | 19 | type Fn = (data: unknown) => typeof validateData; 20 | 21 | /** 22 | * Validate and ignore unknown keys, removing them from the result. 23 | * 24 | * When validating untrusted data, unknown keys should always be removed to 25 | * not result in unwanted parameters or the `__proto__` attribute being 26 | * maliciously passed to internal functions. 27 | */ 28 | export class ParseSafe extends Benchmark { 29 | run() { 30 | this.fn(validateData); 31 | } 32 | 33 | test(describe: SuiteAPI, expect: ExpectStatic, test: TestAPI) { 34 | describe(this.moduleName, () => { 35 | test('should validate the data', () => { 36 | expect(this.fn(validateData)).toEqual(validateData); 37 | }); 38 | 39 | test('should validate with unknown attributes but remove them from the validated result', () => { 40 | const dataWithExtraKeys = { 41 | ...validateData, 42 | extraAttribute: 'foo', 43 | }; 44 | 45 | expect(this.fn(dataWithExtraKeys)).toEqual(validateData); 46 | }); 47 | 48 | // some libraries define the strict / non-strict validation as an 49 | // option to the record/object/type type so we need to test the 50 | // nested extra attribute explicitely so we know our runtype has 51 | // been constructed correctly 52 | test('should validate with unknown attributes but remove them from the validated result (nested)', () => { 53 | const dataWithExtraNestedKeys = { 54 | ...validateData, 55 | deeplyNested: { 56 | ...validateData.deeplyNested, 57 | extraNestedAttribute: 'bar', 58 | }, 59 | }; 60 | 61 | expect(this.fn(dataWithExtraNestedKeys)).toEqual(validateData); 62 | }); 63 | 64 | test('should throw on missing attributes', () => { 65 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 66 | const data: any = { 67 | ...validateData, 68 | }; 69 | 70 | delete data.number; 71 | 72 | expect(() => this.fn(data)).toThrow(); 73 | }); 74 | 75 | test('should throw on data with an invalid attribute', () => { 76 | expect(() => 77 | this.fn({ 78 | ...validateData, 79 | number: 'foo', 80 | }), 81 | ).toThrow(); 82 | }); 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /cases/parse-dont-validate.ts: -------------------------------------------------------------------------------- 1 | import parse, { 2 | parseAsBoolean, 3 | parseAsMutableObject, 4 | parseAsNumber, 5 | parseAsString, 6 | } from 'parse-dont-validate'; 7 | import { addCase } from '../benchmarks'; 8 | 9 | addCase('parse-dont-validate (chained function)', 'parseSafe', data => 10 | parse(data) 11 | .asMutableObject(data => ({ 12 | number: parse(data.number).asNumber().elseThrow('number is not a number'), 13 | negNumber: parse(data.negNumber) 14 | .asNumber() 15 | .inRangeOf({ 16 | max: -1, 17 | min: Number.MIN_SAFE_INTEGER, 18 | }) 19 | .elseThrow('negNumber is not a number'), 20 | maxNumber: parse(data.maxNumber) 21 | .asNumber() 22 | .inRangeOf({ 23 | min: Number.MAX_VALUE, 24 | max: Number.MAX_VALUE, 25 | }) 26 | .elseThrow('maxNumber is not a number'), 27 | string: parse(data.string).asString().elseThrow('string is not a string'), 28 | longString: parse(data.longString) 29 | .asString() 30 | .elseThrow('longString is not a string'), 31 | boolean: parse(data.boolean) 32 | .asBoolean() 33 | .elseThrow('boolean is not a boolean'), 34 | deeplyNested: parse(data.deeplyNested) 35 | .asMutableObject(deeplyNested => ({ 36 | foo: parse(deeplyNested.foo) 37 | .asString() 38 | .elseThrow('foo is not a string'), 39 | num: parse(deeplyNested.num) 40 | .asNumber() 41 | .elseThrow('num is not a number'), 42 | bool: parse(deeplyNested.bool) 43 | .asBoolean() 44 | .elseThrow('bool is not a boolean'), 45 | })) 46 | .elseThrow('deeplyNested is not an object'), 47 | })) 48 | .elseThrow('data is not an object'), 49 | ); 50 | 51 | addCase('parse-dont-validate (named parameters)', 'parseSafe', data => 52 | parseAsMutableObject({ 53 | object: data, 54 | ifParsingFailThen: 'throw', 55 | message: 'data is not an object', 56 | parse: data => ({ 57 | number: parseAsNumber({ 58 | number: data.number, 59 | ifParsingFailThen: 'throw', 60 | message: 'number is not a number', 61 | }), 62 | negNumber: parseAsNumber({ 63 | number: data.negNumber, 64 | ifParsingFailThen: 'throw', 65 | message: 'negNumber is not a number', 66 | inRangeOf: { 67 | max: -1, 68 | min: Number.MIN_SAFE_INTEGER, 69 | }, 70 | }), 71 | maxNumber: parseAsNumber({ 72 | number: data.maxNumber, 73 | ifParsingFailThen: 'throw', 74 | message: 'maxNumber is not a number', 75 | inRangeOf: { 76 | min: Number.MAX_VALUE, 77 | max: Number.MAX_VALUE, 78 | }, 79 | }), 80 | string: parseAsString({ 81 | string: data.string, 82 | ifParsingFailThen: 'throw', 83 | message: 'string is not a string', 84 | }), 85 | longString: parseAsString({ 86 | string: data.longString, 87 | ifParsingFailThen: 'throw', 88 | message: 'longString is not a string', 89 | }), 90 | boolean: parseAsBoolean({ 91 | boolean: data.boolean, 92 | ifParsingFailThen: 'throw', 93 | message: 'boolean is not a boolean', 94 | }), 95 | deeplyNested: parseAsMutableObject({ 96 | object: data.deeplyNested, 97 | parse: deeplyNested => ({ 98 | foo: parseAsString({ 99 | string: deeplyNested.foo, 100 | ifParsingFailThen: 'throw', 101 | message: 'foo is not a string', 102 | }), 103 | num: parseAsNumber({ 104 | number: deeplyNested.num, 105 | ifParsingFailThen: 'throw', 106 | message: 'num is not a number', 107 | }), 108 | bool: parseAsBoolean({ 109 | boolean: deeplyNested.bool, 110 | ifParsingFailThen: 'throw', 111 | message: 'bool is not a boolean', 112 | }), 113 | }), 114 | ifParsingFailThen: 'throw', 115 | message: 'deeplyNested is not an object', 116 | }), 117 | }), 118 | }), 119 | ); 120 | -------------------------------------------------------------------------------- /cases/pure-parse.ts: -------------------------------------------------------------------------------- 1 | import { createCase } from '../benchmarks'; 2 | import { 3 | object, 4 | objectCompiled, 5 | objectGuard, 6 | objectGuardCompiled, 7 | parseString, 8 | parseNumber, 9 | parseBoolean, 10 | type Parser, 11 | type Guard, 12 | isNumber, 13 | isString, 14 | isBoolean, 15 | objectStrict, 16 | objectStrictCompiled, 17 | } from 'pure-parse'; 18 | 19 | /** 20 | * Given a PureParse parser, return a new parser that throws an error if parsing fails, and returns the value if parsing succeeds. 21 | * @param parse 22 | * @returns a parser that is compatible with `createCase` 23 | */ 24 | const tryParse = 25 | (parse: Parser) => 26 | (data: unknown): T => { 27 | const res = parse(data); 28 | if (res.tag === 'failure') { 29 | throw new Error('parsing failed'); 30 | } else { 31 | return res.value; 32 | } 33 | }; 34 | 35 | /** 36 | * Given a PureParse guard, return a new guard that throws an error if parsing fails, and returns the value if parsing succeeds. 37 | * @param guard 38 | * @returns a parser that is compatible with `createCase` 39 | */ 40 | const tryGuard = 41 | (guard: Guard) => 42 | (data: unknown): true => { 43 | const isT = guard(data); 44 | if (!isT) { 45 | throw new Error('validation failed'); 46 | } else { 47 | return true; 48 | } 49 | }; 50 | 51 | createCase('pure-parse (JIT compiled)', 'parseSafe', () => 52 | tryParse( 53 | objectCompiled({ 54 | number: parseNumber, 55 | negNumber: parseNumber, 56 | maxNumber: parseNumber, 57 | string: parseString, 58 | longString: parseString, 59 | boolean: parseBoolean, 60 | deeplyNested: objectCompiled({ 61 | foo: parseString, 62 | num: parseNumber, 63 | bool: parseBoolean, 64 | }), 65 | }), 66 | ), 67 | ); 68 | 69 | createCase('pure-parse', 'parseSafe', () => 70 | tryParse( 71 | object({ 72 | number: parseNumber, 73 | negNumber: parseNumber, 74 | maxNumber: parseNumber, 75 | string: parseString, 76 | longString: parseString, 77 | boolean: parseBoolean, 78 | deeplyNested: object({ 79 | foo: parseString, 80 | num: parseNumber, 81 | bool: parseBoolean, 82 | }), 83 | }), 84 | ), 85 | ); 86 | 87 | createCase('pure-parse', 'parseStrict', () => 88 | tryParse( 89 | objectStrict({ 90 | number: parseNumber, 91 | negNumber: parseNumber, 92 | maxNumber: parseNumber, 93 | string: parseString, 94 | longString: parseString, 95 | boolean: parseBoolean, 96 | deeplyNested: objectStrict({ 97 | foo: parseString, 98 | num: parseNumber, 99 | bool: parseBoolean, 100 | }), 101 | }), 102 | ), 103 | ); 104 | 105 | createCase('pure-parse (JIT compiled)', 'parseStrict', () => 106 | tryParse( 107 | objectStrictCompiled({ 108 | number: parseNumber, 109 | negNumber: parseNumber, 110 | maxNumber: parseNumber, 111 | string: parseString, 112 | longString: parseString, 113 | boolean: parseBoolean, 114 | deeplyNested: objectStrictCompiled({ 115 | foo: parseString, 116 | num: parseNumber, 117 | bool: parseBoolean, 118 | }), 119 | }), 120 | ), 121 | ); 122 | 123 | createCase('pure-parse (JIT compiled)', 'assertLoose', () => 124 | tryGuard( 125 | objectGuardCompiled({ 126 | number: isNumber, 127 | negNumber: isNumber, 128 | maxNumber: isNumber, 129 | string: isString, 130 | longString: isString, 131 | boolean: isBoolean, 132 | deeplyNested: objectGuardCompiled({ 133 | foo: isString, 134 | num: isNumber, 135 | bool: isBoolean, 136 | }), 137 | }), 138 | ), 139 | ); 140 | 141 | createCase('pure-parse', 'assertLoose', () => 142 | tryGuard( 143 | objectGuard({ 144 | number: isNumber, 145 | negNumber: isNumber, 146 | maxNumber: isNumber, 147 | string: isString, 148 | longString: isString, 149 | boolean: isBoolean, 150 | deeplyNested: objectGuard({ 151 | foo: isString, 152 | num: isNumber, 153 | bool: isBoolean, 154 | }), 155 | }), 156 | ), 157 | ); 158 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from 'node:child_process'; 2 | import * as benchmarks from './benchmarks'; 3 | import * as cases from './cases'; 4 | 5 | async function main() { 6 | // a runtype lib would be handy here to check the passed command names ;) 7 | const [command, ...args] = process.argv.slice(2); 8 | 9 | switch (command) { 10 | case undefined: 11 | case 'run': 12 | // run the given or all benchmarks, each in its own node process, see 13 | // https://github.com/moltar/typescript-runtime-type-benchmarks/issues/864 14 | { 15 | console.log('Removing previous results'); 16 | benchmarks.deleteResults(); 17 | 18 | const caseNames = args.length ? args : cases.cases; 19 | 20 | for (const c of caseNames) { 21 | // hack: manually run the spectypes and ts-runtime-checks compilation step - avoids 22 | // having to run it before any other benchmark, esp when working 23 | // locally and checking against a few selected ones. 24 | if (c === 'spectypes') { 25 | childProcess.execSync('npm run compile:spectypes', { 26 | stdio: 'inherit', 27 | }); 28 | } 29 | if (c === 'ts-runtime-checks') { 30 | childProcess.execSync('npm run compile:ts-runtime-checks', { 31 | stdio: 'inherit', 32 | }); 33 | } 34 | if (c === 'typia') { 35 | childProcess.execSync('npm run compile:typia', { 36 | stdio: 'inherit', 37 | }); 38 | } 39 | if (c === 'deepkit') { 40 | childProcess.execSync('npm run compile:deepkit', { 41 | stdio: 'inherit', 42 | }); 43 | } 44 | if (c === 'ts-auto-guard') { 45 | childProcess.execSync('npm run compile:ts-auto-guard', { 46 | stdio: 'inherit', 47 | }); 48 | } 49 | if (c === 'type-predicate-generator') { 50 | childProcess.execSync('npm run compile:type-predicate-generator', { 51 | stdio: 'inherit', 52 | }); 53 | } 54 | if (c === 'paseri') { 55 | childProcess.execSync('npm run compile:paseri', { 56 | stdio: 'inherit', 57 | }); 58 | } 59 | 60 | const cmd = [...process.argv.slice(0, 2), 'run-internal', c]; 61 | 62 | console.log('Executing "%s"', c); 63 | 64 | try { 65 | childProcess.execFileSync(cmd[0], cmd.slice(1), { 66 | shell: false, 67 | stdio: 'inherit', 68 | }); 69 | } catch (e) { 70 | // See #1611. 71 | // Due to the wide range of modules and node versions that we 72 | // benchmark these days, not every library supports every node 73 | // version. 74 | // So we ignore any benchmark that fails in order to not fail the 75 | // whole run benchmark gh action. Their results will not be 76 | // visible for this node version in the frontend. 77 | // The better solution would be benchmark case metadata that lists 78 | // compatible / node versions (e.g. for stnl sth like >= 23) and 79 | // then skip incompatible node versions explicitly. 80 | console.error('Skipped "%s" benchmark due to an error', c); 81 | } 82 | } 83 | } 84 | break; 85 | 86 | case 'create-preview-svg': 87 | // separate command, because preview generation needs the accumulated 88 | // results from the benchmark runs 89 | await benchmarks.createPreviewGraph(); 90 | break; 91 | 92 | case 'run-internal': 93 | // run the given benchmark(s) & append the results 94 | { 95 | const caseNames = args as cases.CaseName[]; 96 | 97 | for (const c of caseNames) { 98 | console.log('Loading "%s"', c); 99 | 100 | try { 101 | await cases.importCase(c); 102 | } catch (e) { 103 | console.log('Error loading %s', c, e); 104 | } 105 | } 106 | 107 | await benchmarks.runAllBenchmarks(); 108 | } 109 | break; 110 | 111 | default: 112 | console.error('unknown command:', command); 113 | 114 | //eslint-disable-next-line n/no-process-exit 115 | process.exit(1); 116 | } 117 | } 118 | 119 | main().catch(e => { 120 | throw e; 121 | }); 122 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | env: 4 | CI: "true" 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | paths: 11 | - .github/workflows/*.yml 12 | - "cases/*.ts" 13 | - "*.ts" 14 | - package.json 15 | - package-lock.json 16 | - bun.lock 17 | 18 | jobs: 19 | build: 20 | name: "Node ${{ matrix.node-version }}" 21 | 22 | runs-on: ubuntu-latest 23 | 24 | strategy: 25 | max-parallel: 1 26 | matrix: 27 | # benchmarked node versions must be kept in sync with: 28 | # - node-version matrix in pr.yml 29 | # - NODE_VERSIONS in app.tsx 30 | # - NODE_VERSION_FOR_PREVIEW in main.ts 31 | node-version: 32 | - 20.x 33 | - 21.x 34 | - 22.x 35 | - 23.x 36 | - 24.x 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - name: Cache node modules 42 | uses: actions/cache@v4 43 | env: 44 | cache-name: cache-node-modules 45 | with: 46 | path: ~/.npm 47 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 48 | restore-keys: | 49 | ${{ runner.os }}-build-${{ env.cache-name }}- 50 | ${{ runner.os }}-build- 51 | ${{ runner.os }}- 52 | 53 | - name: Use Node.js ${{ matrix.node-version }} 54 | uses: actions/setup-node@v4 55 | with: 56 | node-version: ${{ matrix.node-version }} 57 | 58 | - name: npm install 59 | run: npm ci 60 | 61 | - name: lint 62 | run: npm run lint 63 | 64 | - name: test build 65 | run: npm run test:build 66 | 67 | - name: npm test 68 | run: npm t 69 | 70 | - name: generate benchmarks with node 71 | run: ./start.sh NODE 72 | 73 | - name: push 74 | uses: EndBug/add-and-commit@v9 75 | ## prevents forked repos from comitting results in PRs 76 | if: github.repository == 'moltar/typescript-runtime-type-benchmarks' 77 | with: 78 | author_name: ${{ env.GIT_COMMIT_AUTHOR_NAME }} 79 | author_email: ${{ env.GIT_COMMIT_AUTHOR_EMAIL }} 80 | message: 'feat: ${{ matrix.node-version }} adds auto-generated benchmarks and bar graph' 81 | push: true 82 | fetch: true 83 | pull: '--rebase --autostash' 84 | build-bun: 85 | name: "Bun ${{ matrix.bun-version }}" 86 | 87 | runs-on: ubuntu-latest 88 | 89 | strategy: 90 | max-parallel: 1 91 | matrix: 92 | bun-version: 93 | - 1.2.12 94 | 95 | steps: 96 | - uses: actions/checkout@v4 97 | 98 | - name: Use Bun ${{ matrix.bun-version }} 99 | uses: oven-sh/setup-bun@v2 100 | with: 101 | bun-version: ${{ matrix.bun-version }} 102 | 103 | - name: Install 104 | run: bun install 105 | 106 | - name: Lint 107 | run: bun run lint 108 | 109 | - name: Test build 110 | run: bun run test:build 111 | 112 | - name: Test 113 | run: bun run test 114 | 115 | - name: generate benchmarks with bun 116 | run: ./start.sh BUN 117 | 118 | - name: push 119 | uses: EndBug/add-and-commit@v9 120 | ## prevents forked repos from comitting results in PRs 121 | if: github.repository == 'moltar/typescript-runtime-type-benchmarks' 122 | with: 123 | author_name: ${{ env.GIT_COMMIT_AUTHOR_NAME }} 124 | author_email: ${{ env.GIT_COMMIT_AUTHOR_EMAIL }} 125 | message: 'feat: ${{ matrix.bun-version }} adds auto-generated benchmarks and bar graph' 126 | push: true 127 | fetch: true 128 | pull: '--rebase --autostash' 129 | build-deno: 130 | name: "Deno ${{ matrix.deno-version }}" 131 | 132 | runs-on: ubuntu-latest 133 | 134 | strategy: 135 | max-parallel: 1 136 | matrix: 137 | deno-version: 138 | - 2.1.9 139 | 140 | steps: 141 | - uses: actions/checkout@v4 142 | 143 | - uses: actions/setup-node@v4 144 | 145 | - name: Use Deno ${{ matrix.deno-version }} 146 | uses: denoland/setup-deno@v2 147 | with: 148 | deno-version: ${{ matrix.deno-version }} 149 | 150 | - name: Install 151 | run: npm ci 152 | 153 | - name: Lint 154 | run: deno run lint 155 | 156 | - name: Test build 157 | run: deno task test:build 158 | 159 | - name: Test 160 | run: deno task test 161 | 162 | - name: generate benchmarks with deno 163 | run: ./start.sh DENO 164 | 165 | - name: push 166 | uses: EndBug/add-and-commit@v9 167 | ## prevents forked repos from comitting results in PRs 168 | if: github.repository == 'moltar/typescript-runtime-type-benchmarks' 169 | with: 170 | author_name: ${{ env.GIT_COMMIT_AUTHOR_NAME }} 171 | author_email: ${{ env.GIT_COMMIT_AUTHOR_EMAIL }} 172 | message: 'feat: ${{ matrix.deno-version }} adds auto-generated benchmarks and bar graph' 173 | push: true 174 | fetch: true 175 | pull: '--rebase --autostash' 176 | -------------------------------------------------------------------------------- /benchmarks/helpers/graph.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'node:fs'; 2 | import { optimize } from 'svgo'; 3 | import { parse, View } from 'vega'; 4 | import { compile } from 'vega-lite'; 5 | import { availableBenchmarks, type AvailableBenchmarksIds } from './register'; 6 | import type { BenchmarkResult } from './types'; 7 | 8 | interface PreviewGraphParams { 9 | values: BenchmarkResult[]; 10 | filename: string; 11 | } 12 | 13 | export async function writePreviewGraph(params: PreviewGraphParams) { 14 | const svg = await previewGraph(params); 15 | 16 | writeFileSync(params.filename, svg); 17 | } 18 | 19 | function assertNever(x: never): never { 20 | throw new Error(`assert-never: unknown value: ${x}`); 21 | } 22 | 23 | function getBenchmarkLabel(benchmark: AvailableBenchmarksIds) { 24 | switch (benchmark) { 25 | case 'assertLoose': 26 | return 'Loose Assertion'; 27 | case 'assertStrict': 28 | return 'Strict Assertion'; 29 | case 'parseSafe': 30 | return 'Safe Parsing'; 31 | case 'parseStrict': 32 | return 'Strict Parsing'; 33 | default: 34 | assertNever(benchmark); 35 | } 36 | } 37 | 38 | type BenchmarkLabel = ReturnType; 39 | 40 | function median(values: number[]) { 41 | if (!values.length) { 42 | return NaN; 43 | } 44 | 45 | if (values.length % 2) { 46 | return values[(values.length - 1) / 2]; 47 | } 48 | 49 | return (values[values.length / 2 - 1] + values[values.length / 2]) / 2; 50 | } 51 | 52 | interface PreparedResult extends Partial> { 53 | name: string; 54 | } 55 | 56 | // Cheap aggregation of benchmark data. 57 | // For the repeated bar chart, vega-lite expects a numeric value for each 58 | // repeated field (the benchmark label) for each benchmarked library (`name`). 59 | function prepareData(values: BenchmarkResult[], resultCountToInclude = 4) { 60 | const bins = new Map(); 61 | 62 | values.forEach(result => { 63 | let bin = bins.get(result.benchmark); 64 | 65 | if (!bin) { 66 | bins.set(result.benchmark, []); 67 | bin = []; 68 | } 69 | 70 | bin.push(result); 71 | }); 72 | 73 | const preparedResult: PreparedResult[] = []; 74 | 75 | function updateResult( 76 | name: string, 77 | benchmarkLabel: BenchmarkLabel, 78 | ops: number, 79 | ) { 80 | const existing = preparedResult.find(v => v.name === name); 81 | 82 | if (existing) { 83 | existing[benchmarkLabel] = ops; 84 | } else { 85 | preparedResult.push({ name, [benchmarkLabel]: ops }); 86 | } 87 | } 88 | 89 | bins.forEach(v => { 90 | if (!v.length) { 91 | throw new Error('no results in this bin'); 92 | } 93 | 94 | const sorted = v.sort((a, b) => b.ops - a.ops); 95 | 96 | // the N fasted benchmarks 97 | sorted 98 | .slice(0, resultCountToInclude) 99 | .forEach(r => 100 | updateResult( 101 | r.name, 102 | getBenchmarkLabel(r.benchmark as AvailableBenchmarksIds), 103 | r.ops, 104 | ), 105 | ); 106 | }); 107 | 108 | // add median last to make it appear at the bottom of each individual 109 | // barchart 110 | bins.forEach(v => { 111 | const sorted = v.sort((a, b) => b.ops - a.ops); 112 | 113 | // median of the rest as a comparison 114 | updateResult( 115 | '(median)', 116 | getBenchmarkLabel(v[0].benchmark as AvailableBenchmarksIds), 117 | median(sorted.map(x => x.ops)), 118 | ); 119 | }); 120 | 121 | return preparedResult; 122 | } 123 | 124 | // generate a nice preview graph 125 | async function previewGraph({ values }: PreviewGraphParams): Promise { 126 | const vegaSpec = compile({ 127 | repeat: Object.keys(availableBenchmarks).map(b => 128 | getBenchmarkLabel(b as AvailableBenchmarksIds), 129 | ), 130 | columns: 2, 131 | title: { 132 | anchor: 'middle', 133 | offset: 20, 134 | text: 'Top 3 packages for each benchmark + median, (ops count, better ⯈)', 135 | fontWeight: 'normal', 136 | fontSize: 16, 137 | }, 138 | spec: { 139 | data: { 140 | values: prepareData(values, 3), 141 | }, 142 | height: { step: 15 }, 143 | mark: 'bar', 144 | encoding: { 145 | x: { 146 | field: { repeat: 'repeat' }, 147 | type: 'quantitative', 148 | sort: 'ascending', 149 | }, 150 | y: { 151 | field: 'name', 152 | type: 'nominal', 153 | title: null, 154 | // do not sort by name to keep the preparedValues sorting by ops 155 | // instead 156 | // also, we cannot use `sort: '-x'` because that will include every 157 | // top 3 library in every repeated bar chart, regardless whether it 158 | // is in the top 3 of the current benchmark 159 | sort: null, 160 | }, 161 | color: { 162 | field: 'name', 163 | type: 'nominal', 164 | legend: null, 165 | scale: { scheme: 'tableau10' }, 166 | }, 167 | }, 168 | }, 169 | }); 170 | 171 | const view = new View(parse(vegaSpec.spec), { renderer: 'none' }); 172 | const svg = await view.toSVG(); 173 | 174 | const optimizeSvg = await optimize(svg, { 175 | js2svg: { 176 | pretty: true, 177 | }, 178 | }); 179 | 180 | return optimizeSvg.data; 181 | } 182 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "typescript-runtime-type-benchmarks", 4 | "description": "Benchmark Comparison of TypeScript Runtime Type Support Modules", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Roman Filippov", 8 | "email": "rf@romanfilippov.com" 9 | }, 10 | "version": "1.0.0", 11 | "scripts": { 12 | "lint": "gts check", 13 | "lint:fix": "gts fix", 14 | "start": "ts-node index.ts", 15 | "start:bun": "bun index.ts", 16 | "start:deno": "deno -A index.ts", 17 | "test:build": "npm run compile:spectypes && npm run compile:ts-runtime-checks && npm run compile:typebox && npm run compile:typia && npm run compile:deepkit && npm run compile:ts-auto-guard && npm run compile:type-predicate-generator && npm run compile:paseri && tsc --noEmit", 18 | "test": "npm run compile:spectypes && npm run compile:ts-runtime-checks && npm run compile:typebox && npm run compile:typia && npm run compile:deepkit && npm run compile:ts-auto-guard && npm run compile:type-predicate-generator && npm run compile:paseri && vitest run", 19 | "compile:deepkit": "deepkit-type-install && rimraf cases/deepkit/build && tsc -p cases/deepkit/tsconfig.json", 20 | "compile:paseri": "rimraf cases/paseri/build && esbuild cases/paseri/src/index.ts --bundle --minify --platform=node --outdir=cases/paseri/build && tsc --emitDeclarationOnly cases/paseri/src/index.ts --outDir cases/paseri/build", 21 | "compile:spectypes": "rimraf cases/spectypes/build && tsc -p cases/spectypes/src && babel cases/spectypes/src --out-dir cases/spectypes/build --extensions \".ts\"", 22 | "compile:ts-runtime-checks": "rimraf cases/ts-runtime-checks/build && tsc -p cases/ts-runtime-checks/src", 23 | "compile:typebox": "npx -y ts-node cases/typebox/index.ts cases/typebox/build", 24 | "compile:typia": "rimraf cases/typia/build && tsc -p cases/typia/tsconfig.json", 25 | "compile:ts-auto-guard": "rimraf cases/ts-auto-guard/build && ts-auto-guard --project cases/ts-auto-guard/tsconfig.json && tsc -p cases/ts-auto-guard/tsconfig.json", 26 | "compile:type-predicate-generator": "./cases/type-predicate-generator/compile.sh", 27 | "prepare": "ts-patch install", 28 | "download-packages-popularity": "ts-node download-packages-popularity.ts" 29 | }, 30 | "dependencies": { 31 | "@aeriajs/validation": "0.0.162", 32 | "@ailabs/ts-utils": "1.4.0", 33 | "@badrap/valita": "0.4.6", 34 | "@deepkit/core": "1.0.19", 35 | "@deepkit/type": "1.0.19", 36 | "@deepkit/type-compiler": "1.0.19", 37 | "@effect/schema": "0.75.5", 38 | "@mojotech/json-type-validation": "3.1.0", 39 | "@mondrian-framework/model": "2.0.69", 40 | "@sapphire/shapeshift": "3.9.7", 41 | "@sinclair/typebox": "0.34.41", 42 | "@sinclair/typemap": "0.10.1", 43 | "@skarab/tson": "1.5.1", 44 | "@toi/toi": "1.3.0", 45 | "@typeofweb/schema": "0.7.3", 46 | "@types/benchmark": "2.1.5", 47 | "@vbudovski/paseri": "npm:@jsr/vbudovski__paseri@0.1.20", 48 | "ajv": "8.17.1", 49 | "arktype": "2.1.23", 50 | "banditypes": "0.3.0", 51 | "benny": "3.7.1", 52 | "bueno": "0.1.5", 53 | "caketype": "0.5.0", 54 | "class-transformer": "0.5.1", 55 | "class-transformer-validator": "0.9.1", 56 | "class-validator": "0.14.2", 57 | "cleaners": "0.3.17", 58 | "clone": "2.1.2", 59 | "computed-types": "1.11.2", 60 | "csv-stringify": "6.6.0", 61 | "decoders": "1.25.5", 62 | "dhi": "0.4.3", 63 | "fp-ts": "2.16.11", 64 | "io-ts": "2.2.22", 65 | "jet-schema": "1.4.3", 66 | "jet-validators": "1.6.5", 67 | "joi": "17.13.3", 68 | "jointz": "7.0.4", 69 | "json-decoder": "1.4.1", 70 | "mol_data_all": "1.1.1616", 71 | "myzod": "1.12.1", 72 | "ok-computer": "1.0.4", 73 | "parse-dont-validate": "4.0.0", 74 | "preact": "10.27.2", 75 | "pure-parse": "0.0.0-beta.8", 76 | "purify-ts": "2.1.2", 77 | "r-assign": "1.9.0", 78 | "reflect-metadata": "0.2.2", 79 | "rescript-schema": "9.2.2", 80 | "rulr": "10.8.2", 81 | "runtypes": "6.7.0", 82 | "simple-runtypes": "7.1.3", 83 | "spectypes": "2.1.11", 84 | "stnl": "1.1.6", 85 | "succulent": "0.18.1", 86 | "superstruct": "2.0.2", 87 | "suretype": "2.4.1", 88 | "sury": "10.0.4", 89 | "svgo": "3.3.2", 90 | "tiny-schema-validator": "5.0.3", 91 | "to-typed": "0.5.2", 92 | "ts-auto-guard": "5.0.1", 93 | "ts-interface-checker": "1.0.2", 94 | "ts-json-validator": "0.7.1", 95 | "ts-node": "10.9.2", 96 | "ts-runtime-checks": "0.6.3", 97 | "type-predicate-generator": "1.0.4", 98 | "typescript": "5.9.3", 99 | "typia": "9.7.2", 100 | "undici": "7.16.0", 101 | "unknownutil": "3.18.1", 102 | "valibot": "1.2.0", 103 | "vality": "6.3.4", 104 | "vega": "5.33.0", 105 | "vega-lite": "5.11.0", 106 | "yup": "1.7.1", 107 | "zod": "3.25.76", 108 | "zod4": "npm:zod@next" 109 | }, 110 | "devDependencies": { 111 | "@babel/cli": "7.28.3", 112 | "@babel/core": "7.28.4", 113 | "@babel/preset-env": "7.28.3", 114 | "@babel/preset-typescript": "7.27.1", 115 | "@types/clone": "2.1.4", 116 | "@types/node": "22.18.11", 117 | "@types/svgo": "3.0.0", 118 | "@types/ts-expose-internals": "npm:ts-expose-internals@5.6.3", 119 | "@types/yup": "0.32.0", 120 | "babel-plugin-spectypes": "2.1.11", 121 | "esbuild": "0.25.11", 122 | "expect-type": "1.2.2", 123 | "gts": "6.0.2", 124 | "rimraf": "6.0.1", 125 | "ts-patch": "3.3.0", 126 | "tsconfigs": "4.0.2", 127 | "vitest": "3.2.4" 128 | }, 129 | "keywords": [ 130 | "benchmarks", 131 | "types", 132 | "typescript" 133 | ] 134 | } 135 | -------------------------------------------------------------------------------- /benchmarks/helpers/main.ts: -------------------------------------------------------------------------------- 1 | import { add, complete, cycle, suite } from 'benny'; 2 | import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs'; 3 | import { join, dirname } from 'node:path'; 4 | import { writePreviewGraph } from './graph'; 5 | import { getRegisteredBenchmarks } from './register'; 6 | import type { BenchmarkCase, BenchmarkResult } from './types'; 7 | 8 | /** 9 | * A getDirname that works in CJS and ESM, since this file is directly shared 10 | * across both kinds of projects. 11 | * @TODO We can remove this once we've migrated all consumers to ESM. 12 | * 13 | * @see https://stackoverflow.com/a/79251101/13503626 14 | */ 15 | function pathFromStack() { 16 | const { stack } = new Error(); 17 | if (!stack) { 18 | throw new Error('Could not get stack'); 19 | } 20 | const lines = stack.split('\n'); 21 | for (const line of lines) { 22 | if (line.includes(' (/') || line.includes(' (file://')) { 23 | // assumes UNIX-like paths 24 | const location = line.split(' (')[1].replace('file://', ''); 25 | const filepath = location.split(':')[0]; 26 | const dirpath = dirname(filepath); 27 | return { dirpath, filepath }; 28 | } 29 | } 30 | throw new Error('Could not get dirname'); 31 | } 32 | 33 | function getRuntimeWithVersion() { 34 | // @ts-expect-error no @types/bun 35 | if (typeof Bun !== 'undefined') { 36 | return { RUNTIME: 'bun', RUNTIME_VERSION: process.versions['bun']! }; 37 | } 38 | 39 | // @ts-expect-error no Deno types 40 | if (typeof Deno !== 'undefined') { 41 | return { RUNTIME: 'deno', RUNTIME_VERSION: process.versions['deno']! }; 42 | } 43 | 44 | return { RUNTIME: 'node', RUNTIME_VERSION: process.version }; 45 | } 46 | 47 | const DOCS_DIR = join(pathFromStack().dirpath, '../../docs'); 48 | const { RUNTIME, RUNTIME_VERSION } = getRuntimeWithVersion(); 49 | const RUNTIME_FOR_PREVIEW = 'node'; 50 | const NODE_VERSION_FOR_PREVIEW = 20; 51 | 52 | /** 53 | * Run all registered benchmarks and append the results to a file. 54 | */ 55 | export async function runAllBenchmarks() { 56 | const allResults: BenchmarkResult[] = []; 57 | 58 | for (const [benchmark, benchmarks] of getRegisteredBenchmarks()) { 59 | const summary = await runBenchmarks(benchmark, benchmarks); 60 | 61 | if (!summary) { 62 | continue; 63 | } 64 | 65 | summary.results.forEach(({ name, ops, margin }) => { 66 | allResults.push({ 67 | benchmark, 68 | name, 69 | ops, 70 | margin, 71 | runtime: RUNTIME, 72 | runtimeVersion: RUNTIME_VERSION, 73 | }); 74 | }); 75 | } 76 | 77 | // collect results of isolated benchmark runs into a single file 78 | appendResults(allResults); 79 | } 80 | 81 | /** 82 | * Remove the results json file. 83 | */ 84 | export function deleteResults() { 85 | const fileName = resultsJsonFilename(); 86 | 87 | if (existsSync(fileName)) { 88 | unlinkSync(fileName); 89 | } 90 | } 91 | 92 | /** 93 | * Generate the preview svg shown in the readme. 94 | */ 95 | export async function createPreviewGraph() { 96 | const majorVersion = getNodeMajorVersion(); 97 | 98 | if ( 99 | majorVersion === NODE_VERSION_FOR_PREVIEW && 100 | RUNTIME_FOR_PREVIEW === 'node' 101 | ) { 102 | const allResults: BenchmarkResult[] = JSON.parse( 103 | readFileSync(resultsJsonFilename()).toString(), 104 | ).results; 105 | 106 | await writePreviewGraph({ 107 | filename: previewSvgFilename(), 108 | values: allResults, 109 | }); 110 | } 111 | } 112 | 113 | // run a benchmark fn with benny 114 | async function runBenchmarks(name: string, cases: BenchmarkCase[]) { 115 | if (cases.length === 0) { 116 | return; 117 | } 118 | 119 | const fns = cases.map(c => add(c.moduleName, () => c.run())); 120 | 121 | return suite( 122 | name, 123 | 124 | // benchmark functions 125 | ...fns, 126 | 127 | cycle(), 128 | complete(), 129 | ); 130 | } 131 | 132 | // append results to an existing file or create a new one 133 | function appendResults(results: BenchmarkResult[]) { 134 | const fileName = resultsJsonFilename(); 135 | 136 | const existingResults: BenchmarkResult[] = existsSync(fileName) 137 | ? JSON.parse(readFileSync(fileName).toString()).results 138 | : []; 139 | 140 | // check that we're appending unique data 141 | const getKey = ({ 142 | benchmark, 143 | name, 144 | runtime, 145 | runtimeVersion, 146 | }: BenchmarkResult): string => { 147 | return JSON.stringify({ benchmark, name, runtime, runtimeVersion }); 148 | }; 149 | const existingResultsIndex = new Set(existingResults.map(r => getKey(r))); 150 | 151 | results.forEach(r => { 152 | if (existingResultsIndex.has(getKey(r))) { 153 | console.error('Result %s already exists in', getKey(r), fileName); 154 | 155 | throw new Error('Duplicate result in result json file'); 156 | } 157 | }); 158 | 159 | writeFileSync( 160 | fileName, 161 | 162 | JSON.stringify({ 163 | results: [...existingResults, ...results], 164 | }), 165 | 166 | { encoding: 'utf8' }, 167 | ); 168 | } 169 | 170 | function resultsJsonFilename() { 171 | const majorVersion = getNodeMajorVersion(); 172 | 173 | return join(DOCS_DIR, 'results', `${RUNTIME}-${majorVersion}.json`); 174 | } 175 | 176 | function previewSvgFilename() { 177 | return join(DOCS_DIR, 'results', 'preview.svg'); 178 | } 179 | 180 | function getNodeMajorVersion() { 181 | // Hack for bun runtime to include major and minor version 182 | // like 1.2.3 -> 1.2 183 | if (RUNTIME === 'bun') { 184 | return parseFloat(RUNTIME_VERSION); 185 | } 186 | 187 | let majorVersion = 0; 188 | 189 | majorVersion = parseInt(RUNTIME_VERSION); 190 | 191 | if (!isNaN(majorVersion)) { 192 | return majorVersion; 193 | } 194 | 195 | majorVersion = parseInt(RUNTIME_VERSION.slice(1)); 196 | 197 | if (!isNaN(majorVersion)) { 198 | return majorVersion; 199 | } 200 | 201 | return majorVersion; 202 | } 203 | -------------------------------------------------------------------------------- /download-packages-popularity.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { request } from 'undici'; 3 | 4 | export const packages = [ 5 | { 6 | name: 'aeria', 7 | packageName: '@aeriajs/validation', 8 | }, 9 | { 10 | name: 'ajv', 11 | packageName: 'ajv', 12 | }, 13 | { 14 | name: 'arktype', 15 | packageName: 'arktype', 16 | }, 17 | { 18 | name: 'banditypes', 19 | packageName: 'banditypes', 20 | }, 21 | { 22 | name: 'bueno', 23 | packageName: 'bueno', 24 | }, 25 | { 26 | name: 'caketype', 27 | packageName: 'caketype', 28 | }, 29 | { 30 | name: 'class-transformer-validator-sync', 31 | packageName: 'class-validator', 32 | }, 33 | { 34 | name: 'computed-types', 35 | packageName: 'computed-types', 36 | }, 37 | { 38 | name: 'decoders', 39 | packageName: 'decoders', 40 | }, 41 | { 42 | name: 'io-ts', 43 | packageName: 'io-ts', 44 | }, 45 | { 46 | name: 'jointz', 47 | packageName: 'jointz', 48 | }, 49 | { 50 | name: 'json-decoder', 51 | packageName: 'json-decoder', 52 | }, 53 | { 54 | name: '$mol_data', 55 | packageName: 'mol_data_all', 56 | }, 57 | { 58 | name: '@mojotech/json-type-validation', 59 | packageName: '@mojotech/json-type-validation', 60 | }, 61 | { 62 | name: 'mondrian-framework', 63 | packageName: '@mondrian-framework/model', 64 | }, 65 | { 66 | name: 'myzod', 67 | packageName: 'myzod', 68 | }, 69 | { 70 | name: 'ok-computer', 71 | packageName: 'ok-computer', 72 | }, 73 | { 74 | name: 'parse-dont-validate (chained function)', 75 | packageName: 'parse-dont-validate', 76 | }, 77 | { 78 | name: 'parse-dont-validate (named parameters)', 79 | packageName: 'parse-dont-validate', 80 | }, 81 | { 82 | name: 'purify-ts', 83 | packageName: 'purify-ts', 84 | }, 85 | { 86 | name: 'r-assign', 87 | packageName: 'r-assign', 88 | }, 89 | { 90 | name: 'rescript-schema', 91 | packageName: 'rescript-schema', 92 | }, 93 | { 94 | name: 'rulr', 95 | packageName: 'rulr', 96 | }, 97 | { 98 | name: 'runtypes', 99 | packageName: 'runtypes', 100 | }, 101 | { 102 | name: '@sapphire/shapeshift', 103 | packageName: '@sapphire/shapeshift', 104 | }, 105 | { 106 | name: 'simple-runtypes', 107 | packageName: 'simple-runtypes', 108 | }, 109 | { 110 | name: '@sinclair/typebox-(ahead-of-time)', 111 | packageName: '@sinclair/typebox', 112 | }, 113 | { 114 | name: '@sinclair/typebox-(dynamic)', 115 | packageName: '@sinclair/typebox', 116 | }, 117 | { 118 | name: '@sinclair/typebox-(just-in-time)', 119 | packageName: '@sinclair/typebox', 120 | }, 121 | { 122 | name: 'spectypes', 123 | packageName: 'spectypes', 124 | }, 125 | { 126 | name: 'succulent', 127 | packageName: 'succulent', 128 | }, 129 | { 130 | name: 'superstruct', 131 | packageName: 'superstruct', 132 | }, 133 | { 134 | name: 'suretype', 135 | packageName: 'suretype', 136 | }, 137 | { 138 | name: 'sury', 139 | packageName: 'sury', 140 | }, 141 | { 142 | name: 'tiny-schema-validator', 143 | packageName: 'tiny-schema-validator', 144 | }, 145 | { 146 | name: 'to-typed', 147 | packageName: 'to-typed', 148 | }, 149 | { 150 | name: 'toi', 151 | packageName: '@toi/toi', 152 | }, 153 | { 154 | name: 'ts-interface-checker', 155 | packageName: 'ts-interface-checker', 156 | }, 157 | { 158 | name: 'ts-json-validator', 159 | packageName: 'ts-json-validator', 160 | }, 161 | { 162 | name: 'ts-runtime-checks', 163 | packageName: 'ts-runtime-checks', 164 | }, 165 | { 166 | name: 'ts-utils', 167 | packageName: '@ailabs/ts-utils', 168 | }, 169 | { 170 | name: 'tson', 171 | packageName: '@skarab/tson', 172 | }, 173 | { 174 | name: '@typeofweb/schema', 175 | packageName: '@typeofweb/schema', 176 | }, 177 | { 178 | name: 'typia', 179 | packageName: 'typia', 180 | }, 181 | { 182 | name: 'unknownutil', 183 | packageName: 'unknownutil', 184 | }, 185 | { 186 | name: 'valibot', 187 | packageName: 'valibot', 188 | }, 189 | { 190 | name: 'valita', 191 | packageName: '@badrap/valita', 192 | }, 193 | { 194 | name: 'vality', 195 | packageName: 'vality', 196 | }, 197 | { 198 | name: 'yup', 199 | packageName: 'yup', 200 | }, 201 | { 202 | name: 'zod', 203 | packageName: 'zod', 204 | }, 205 | { 206 | name: 'deepkit', 207 | packageName: '@deepkit/core', 208 | }, 209 | { 210 | name: 'effect-schema', 211 | packageName: '@effect/schema', 212 | }, 213 | { 214 | name: 'ts-auto-guard', 215 | packageName: 'ts-auto-guard', 216 | }, 217 | { 218 | name: 'type-predicate-generator', 219 | packageName: 'type-predicate-generator', 220 | }, 221 | { 222 | name: 'joi', 223 | packageName: 'joi', 224 | }, 225 | ] as const; 226 | 227 | interface BodyWeeklyDownloads { 228 | downloads: number; 229 | start: Date; 230 | end: Date; 231 | package: string; 232 | } 233 | 234 | async function getWeeklyDownloads(packageName: string) { 235 | try { 236 | const response = await request( 237 | `https://api.npmjs.org/downloads/point/last-week/${packageName}`, 238 | ).then(response => response.body.json() as Promise); 239 | 240 | return response.downloads; 241 | } catch (error) { 242 | console.error('Error fetching download data:', error); 243 | } 244 | } 245 | 246 | const packagesData: { 247 | name: string; 248 | weeklyDownloads: number; 249 | }[] = []; 250 | 251 | async function main() { 252 | for (const { name, packageName } of packages) { 253 | console.log(`Downloading ${name}`); 254 | 255 | const weeklyDownloads = await getWeeklyDownloads(packageName); 256 | 257 | if (typeof weeklyDownloads !== 'number') { 258 | console.error(`No weekly downloads found for ${packageName}`); 259 | 260 | continue; 261 | } 262 | 263 | packagesData.push({ 264 | name, 265 | weeklyDownloads, 266 | }); 267 | } 268 | 269 | fs.writeFileSync( 270 | './docs/packagesPopularity.json', 271 | JSON.stringify(packagesData), 272 | ); 273 | } 274 | 275 | main().catch(error => { 276 | console.error('Error:', error); 277 | }); 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📊 Benchmark Comparison of Packages with Runtime Validation and TypeScript Support 2 | 3 | - - - - 4 | **⚡⚠ Benchmark results have changed after switching to isolated node processes for each benchmarked package, see [#864](https://github.com/moltar/typescript-runtime-type-benchmarks/issues/864) ⚠⚡** 5 | - - - - 6 | 7 | ## Benchmark Results 8 | 9 | [![Fastest Packages - click to view details](docs/results/preview.svg)](https://moltar.github.io/typescript-runtime-type-benchmarks) 10 | 11 | [click here for result details](https://moltar.github.io/typescript-runtime-type-benchmarks) 12 | 13 | ## Packages Compared 14 | 15 | * [aeria](https://github.com/aeria-org/aeria) 16 | * [ajv](https://ajv.js.org/) 17 | * [ArkType](https://github.com/arktypeio/arktype) 18 | * [banditypes](https://github.com/thoughtspile/banditypes) 19 | * [bueno](https://github.com/philipnilsson/bueno) 20 | * [caketype](https://github.com/justinyaodu/caketype) 21 | * [class-validator](https://github.com/typestack/class-validator) + [class-transformer](https://github.com/typestack/class-transformer) 22 | * [cleaners](https://cleaners.js.org) 23 | * [computed-types](https://github.com/neuledge/computed-types) 24 | * [decoders](https://github.com/nvie/decoders) 25 | * [deepkit](https://deepkit.io/) 26 | * [dhi](https://github.com/justrach/dhi-zig/tree/main/js-bindings) 27 | * [@effect/schema](https://github.com/Effect-TS/effect/blob/main/packages/schema/README.md) 28 | * [io-ts](https://github.com/gcanti/io-ts) 29 | * [jet-validators](https://github.com/seanpmaxwell/jet-validators) 30 | * [joi](https://github.com/hapijs/joi) 31 | * [jointz](https://github.com/moodysalem/jointz) 32 | * [json-decoder](https://github.com/venil7/json-decoder) 33 | * [@mojotech/json-type-validaton](https://github.com/mojotech/json-type-validation) 34 | * [$mol_data](https://github.com/hyoo-ru/mam_mol/blob/master/data/README.md) 35 | * [@mondrian-framework/model](https://mondrianframework.com) 36 | * [myzod](https://github.com/davidmdm/myzod) 37 | * [ok-computer](https://github.com/richardscarrott/ok-computer) 38 | * [pure-parse](https://github.com/johannes-lindgren/pure-parse) 39 | * [purify-ts](https://github.com/gigobyte/purify) 40 | * [parse-dont-validate](https://github.com/Packer-Man/parse-dont-validate) 41 | * [Paseri](https://github.com/vbudovski/paseri) 42 | * [r-assign](https://github.com/micnic/r-assign) 43 | * [rescript-schema](https://github.com/DZakh/rescript-schema) 44 | * [rulr](https://github.com/ryansmith94/rulr) 45 | * [runtypes](https://github.com/pelotom/runtypes) 46 | * [@sapphire/shapeshift](https://github.com/sapphiredev/shapeshift) 47 | * [@sinclair/typebox](https://github.com/sinclairzx81/typebox) 48 | * [@sinclair/typemap](https://github.com/sinclairzx81/typemap) 49 | * [simple-runtypes](https://github.com/hoeck/simple-runtypes) 50 | * [spectypes](https://github.com/iyegoroff/spectypes) 51 | * [stnl](https://github.com/re-utils/stnl) 52 | * [succulent](https://github.com/aslilac/succulent) 53 | * [superstruct](https://github.com/ianstormtaylor/superstruct) 54 | * [suretype](https://github.com/grantila/suretype) 55 | * [sury](https://github.com/DZakh/sury) 56 | * [tiny-schema-validator](https://github.com/5alidz/tiny-schema-validator) 57 | * [to-typed](https://github.com/jsoldi/to-typed) 58 | * [toi](https://github.com/hf/toi) 59 | * [ts-auto-guard](https://github.com/rhys-vdw/ts-auto-guard) 60 | * [ts-interface-checker](https://github.com/gristlabs/ts-interface-checker) 61 | * [ts-json-validator](https://github.com/ostrowr/ts-json-validator) 62 | * [ts-runtime-checks](https://github.com/GoogleFeud/ts-runtime-checks) 63 | * [tson](https://github.com/skarab42/tson) 64 | * [ts-utils](https://github.com/ai-labs-team/ts-utils) 65 | * [type-predicate-generator](https://github.com/peter-leonov/typescript-predicate-generator) 66 | * [typia](https://github.com/samchon/typia) 67 | * [@typeofweb/schema](https://github.com/typeofweb/schema) 68 | * [unknownutil](https://github.com/lambdalisue/deno-unknownutil) 69 | * [valibot](https://github.com/fabian-hiller/valibot) 70 | * [valita](https://github.com/badrap/valita) 71 | * [Vality](https://github.com/jeengbe/vality) 72 | * [yup](https://github.com/jquense/yup) 73 | * [zod](https://github.com/colinhacks/zod) 74 | * [zod (v4)](https://github.com/colinhacks/zod/tree/v4) 75 | 76 | ## Criteria 77 | 78 | ### Validation 79 | 80 | These packages are capable of validating the data for type correctness. 81 | 82 | E.g. if `string` was expected, but a `number` was provided, the validator should fail. 83 | 84 | ### Interface 85 | 86 | It has a validator function or method that returns a valid type casted value or throws. 87 | 88 | ```ts 89 | const data: any = {} 90 | 91 | // `res` is now type casted to the right type 92 | const res = isValid(data) 93 | ``` 94 | 95 | Or it has a type guard function that in a truthy block type casts the value. 96 | 97 | ```ts 98 | const data: any = {} 99 | 100 | function isMyDataValid(data: any) { 101 | // isValidGuard is the type guard function provided by the package 102 | if (isValidGuard(data)) { 103 | // data here is "guarded" and therefore inferred to be of the right type 104 | return data 105 | } 106 | 107 | throw new Error('Invalid!') 108 | } 109 | 110 | // `res` is now type casted to the right type 111 | const res = isMyDataValid(data) 112 | ``` 113 | 114 | ## Local Development 115 | 116 | ### Commands 117 | 118 | #### Benchmarks 119 | 120 | * `npm run start` - run benchmarks for all modules using Node.js 121 | * `npm run start:bun` - run benchmarks for all modules using bun 122 | * `npm run start run zod myzod valita` - run benchmarks only for a few selected modules 123 | 124 | #### Tests 125 | 126 | * `npm run test` - run build process and tests for all modules 127 | * `npm run test:build` - run build process for all modules 128 | 129 | #### Benchmark Viewer 130 | 131 | A basic preact+vite app lives in [`/docs`](/docs). 132 | It is deployed via github pages whenever something has been pushed to the main branch. 133 | 134 | ```sh 135 | cd docs 136 | 137 | npm run dev # develop / view results 138 | npm run build # build 139 | npm run preview # preview the build 140 | ``` 141 | 142 | When viewing results locally, you will need to restart the app whenever the 143 | results are updated. 144 | 145 | #### Linting 146 | 147 | * `npm run lint` - lint all files 148 | * `npm run lint:fix` - lint all files and fix errors 149 | 150 | #### Misc 151 | 152 | * `npm run download-packages-popularity` - download popularity data from npmjs.com 153 | 154 | ### Debugging 155 | 156 | #### Node.js 157 | 158 | * Use [nvm](https://github.com/nvm-sh/nvm) to switch to a specific Node.js version 159 | * `nvm use x` - switch to Node.js x.x 160 | * `nvm use 18` - switch to Node.js 18.x 161 | * `nvm use 20` - switch to Node.js 20.x 162 | 163 | #### Bun 164 | 165 | * Use `curl -fsSl https://bun.sh/install | bash -s "bun-v1.0.x"` to switch to a specific bun version 166 | * `curl -fsSl https://bun.sh/install | bash -s "bun-v1.1.43"` - switch to bun 1.1.43 167 | 168 | #### Deno 169 | 170 | * Use `deno upgrade x.x.x` to switch to a specific Deno version 171 | * `deno upgrade stable` - switch to Deno x.x.x 172 | 173 | ## Adding new runtime version 174 | 175 | ### Node.js runtime 176 | 177 | * update Node.js version matrix in `.github/workflows/pr.yml` and `.github/workflows/release.yml` 178 | * update `NODE_VERSIONS` in `docs/src/App.tsx` 179 | * optionally set `NODE_VERSION_FOR_PREVIEW` in `benchmarks/helpers/main.ts` 180 | 181 | ### Bun runtime 182 | 183 | * update bun version matrix in `.github/workflows/pr.yml` and `.github/workflows/release.yml` 184 | * update `BUN_VERSIONS` in `docs/src/App.tsx` 185 | 186 | ### Deno runtime 187 | 188 | * update Deno version matrix in `.github/workflows/pr.yml` and `.github/workflows/release.yml` 189 | * update `DENO_VERSIONS` in `docs/src/App.tsx` 190 | 191 | ## Test cases 192 | 193 | * **Safe Parsing** 194 | * Checks the input object against a schema and returns it. 195 | * Raises an error if the input object does not conform to the schema (e.g., a type mismatch or missing attribute). 196 | * Removes any extra keys in the input object that are not defined in the schema. 197 | 198 | * **Strict Parsing** 199 | * Checks the input object against a schema and returns it. 200 | * Raises an error if the input object does not conform to the schema (e.g., a type mismatch or missing attribute). 201 | * Raises an error if the input object contains extra keys. 202 | 203 | * **Loose Assertion** 204 | * Checks the input object against a schema. 205 | * Raises an exception if the input object does not match the schema. 206 | * Allows extra keys without raising errors. 207 | * Returns true if data is valid. 208 | 209 | * **Strict Assertion** 210 | * Checks the input object against a schema. 211 | * Raises an exception if the input object does not match the schema. 212 | * Raises an error if the input object or any nested input objects contain extra keys. 213 | * Returns true if data is valid. 214 | 215 | ## Contributors 216 | 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /docs/results/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 0 20 | 120,000,000 21 | 22 | 23 | Safe Parsing 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | typia 32 | sury 33 | rescript-schema 34 | (median) 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 0 60 | 40,000,000 61 | 62 | 63 | Strict Parsing 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | typia 72 | sury 73 | ts-runtime-checks 74 | (median) 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 0 100 | 150,000,000 101 | 102 | 103 | Loose Assertion 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | ts-runtime-checks 112 | type-predicate-genera… 113 | ts-auto-guard 114 | (median) 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 0 140 | 20,000,000 141 | 40,000,000 142 | 143 | 144 | Strict Assertion 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | ts-runtime-checks 153 | @sinclair/typebox-(ah… 154 | @sinclair/typebox-(ju… 155 | (median) 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | Top 3 packages for each benchmark + median, (ops count, better ⯈) 169 | 170 | 171 | 172 | --------------------------------------------------------------------------------