├── .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 | [](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 |
172 |
--------------------------------------------------------------------------------