Skip to content

Commit f6ea925

Browse files
committed
feat: add Valibot, utility functions, enhance readme
1 parent a0e67fd commit f6ea925

File tree

11 files changed

+243
-73
lines changed

11 files changed

+243
-73
lines changed

.vscode/settings.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
{
22
"editor.quickSuggestions": {
3-
"strings": "on"
3+
"strings": "on"
44
},
55
// prioritize ArkType's "type" for autoimports
66
"typescript.preferences.autoImportSpecifierExcludeRegexes": [
7-
"^(node:)?os$"
7+
"^(node:)?os$"
8+
],
9+
"cSpell.words": [
10+
"arktype",
11+
"valibot"
812
],
913
}

README.md

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,83 @@
1-
# validation-lib-comparaison
1+
# Validation lib comparaison
22

3-
To install dependencies:
3+
## The goal of this project
4+
5+
The goal is to compare different libraries and approaches to validate data in a TypeScript ecosystem.
6+
7+
It's interesting to see how each library is designed, how it's used, and how it performs.
8+
9+
Ideally you can use this comparison to choose the library that fits your needs the best.
10+
11+
## Run tests and benchmarks
12+
13+
Make sure you have [Bun](https://bun.sh) v1.1.x then :
414

515
```bash
16+
# install the dependencies
617
bun install
18+
# run each file once just to verify that everything is working as expected
19+
bun bench --runs 1
20+
# run the benchmarks
21+
bun bench --runs 50
722
```
823

9-
To run:
24+
At the time of writing, Node 22 is around 2 times slower than Bun v1 ^^'
1025

11-
```bash
12-
bun run index.ts
13-
```
26+
## The comparison results
27+
28+
Here are the library used in this comparison :
29+
30+
- [arktype](https://github.com/arktypeio/arktype) v2.0.4 from David Blass
31+
- [valibot](https://github.com/fabian-hiller/valibot) v1.0.0-rc.1 from Fabian Hiller
32+
- [zod](https://github.com/colinhacks/zod) v3.24.2 from Colin McDonnell
33+
34+
| Date | Score | Library | Deps | Size | Light | Input | Throw | Safe | Execution | Fast |
35+
| ---------- | :---: | :-----: | :---: | :----: | :---: | :---: | :---: | :---: | :-------: | :---: |
36+
| 2025-02-21 | 2 | arktype | 2 | 133 KB | 0 | **1** | 0 | **1** | 155 ms | 0 |
37+
| 2025-02-21 | 6 | valibot | **0** | 4 KB | **2** | **1** | **1** | **1** | 74 ms | **1** |
38+
| 2025-02-21 | 4 | zod | **0** | 62 KB | 0 | **1** | **1** | **1** | 75 ms | **1** |
39+
40+
Legend :
41+
42+
- Deps : the number of dependencies of the library
43+
- Size : the minified build size in bytes of the related file in `src`, run `bun run build` to see by yourself
44+
- Light : 1 point if the build is less than 10 KB, 1 bonus point if it's less than 5 KB
45+
- Input : 1 point if the library can see that `age` is optional in the input but not optional in `type User` the output type
46+
- Throw : 1 point if the library have a parse or throw method, useful when we don't want to handle the error cases
47+
- Safe : 1 point if the library have a safe parse method that will not throw and usually return a `Result` type
48+
- Execution : average time in milliseconds to execute the test file with bun, check the `bun run bench` command output
49+
- Fast : 1 point if the library execution time is less than 100 ms
50+
51+
## My favorite pick
52+
53+
Valibot is my favorite pick because it's fast as Zod but has a lighter impact on the bundle size.
54+
55+
## Todo
56+
57+
- [ ] vanilla version ?
58+
- [ ] check error messages
59+
- [ ] check custom error messages
60+
- [ ] check custom validation rules
61+
62+
## How to contribute
63+
64+
There are many ways to contribute to this project :
65+
66+
- Add a new library to the comparison
67+
- Improve the comparison table, criteria, or scoring
68+
- Improve the samples in `src`
69+
- Find a way to enhance library isolation to have more meaningful benchmarks
70+
- Whatever you think that could be useful, check the above todo list for inspiration
71+
72+
Just open an issue or a PR, I'll be happy to discuss it with you <3
73+
74+
## Thanks
1475

15-
This project was created using `bun init` in bun v1.1.8. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
76+
- [Arktype](https://github.com/arktypeio/arktype) : for making this comparison possible
77+
- [Bun](https://bun.sh) : for their great tool to do almost everything ^^
78+
- [Github](https://github.com) : for all their great work year after year, pushing OSS forward
79+
- [Repo-checker](https://github.com/Shuunen/repo-checker) : eslint cover /src code and this tool the rest ^^
80+
- [Shields.io](https://shields.io) : for the nice badges on top of this readme
81+
- [Valibot](https://github.com/fabian-hiller/valibot) : for making this comparison possible
82+
- [Zod](https://github.com/colinhacks/zod) : for making this comparison possible
83+

arktype.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

bun.lock

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,41 @@
11
{
2-
"lockfileVersion": 1,
2+
"lockfileVersion": 0,
33
"workspaces": {
44
"": {
5-
"name": "validation-lib-comparaison",
65
"dependencies": {
7-
"arktype": "^2.0.0",
8-
"zod": "^3.24.1",
6+
"arktype": "^2.0.4",
7+
"valibot": "^1.0.0-rc.1",
8+
"zod": "^3.24.2",
99
},
1010
"devDependencies": {
1111
"@types/bun": "latest",
1212
},
1313
"peerDependencies": {
14-
"typescript": "^5.0.0",
14+
"typescript": "^5.7.3",
1515
},
1616
},
1717
},
1818
"packages": {
19-
"@ark/schema": ["@ark/schema@0.36.0", "", { "dependencies": { "@ark/util": "0.36.0" } }, "sha512-1zL+9AjNk6fNcCp5krt3HxM80kFr+MskqH2Gbz60GzXsjsAVZ38xM5WFVZMEPL8VbmTqDvBiwPm7HvK972lADg=="],
19+
"@ark/schema": ["@ark/schema@0.39.0", "", { "dependencies": { "@ark/util": "0.39.0" } }, "sha512-LQbQUb3Sj461LgklXObAyUJNtsUUCBxZlO2HqRLYvRSqpStm0xTMrXn51DwBNNxeSULvKVpXFwoxiSec9kwKww=="],
2020

21-
"@ark/util": ["@ark/util@0.36.0", "", {}, "sha512-3tn3udpNeoLdmU1kvJYjDnhXereB2PYHED2c2j8Q7OsIRMx+AlOCXWjsI4qjAarjjbfCsKtsdMlE2QnLrcVCuA=="],
21+
"@ark/util": ["@ark/util@0.39.0", "", {}, "sha512-90APHVklk8BP4kku7hIh1BgrhuyKYqoZ4O7EybtFRo7cDl9mIyc/QUbGvYDg//73s0J2H0I/gW9pzroA1R4IBQ=="],
2222

23-
"@types/bun": ["@types/[email protected].0", "", { "dependencies": { "bun-types": "1.2.0" } }, "sha512-5N1JqdahfpBlAv4wy6svEYcd/YfO2GNrbL95JOmFx8nkE6dbK4R0oSE5SpBA4vBRqgrOUAXF8Dpiz+gi7r80SA=="],
23+
"@types/bun": ["@types/[email protected].2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
2424

25-
"@types/node": ["@types/node@22.10.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww=="],
25+
"@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="],
2626

2727
"@types/ws": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
2828

29-
"arktype": ["[email protected].1", "", { "dependencies": { "@ark/schema": "0.36.0", "@ark/util": "0.36.0" } }, "sha512-IfczIefYOd2Omac9J1MH8YKmhGe4pQor1rg3w9mc6m8DA9fSnBRI7GF0XP8GQiHH5Khlj6MS6J1ZPbt68HDbrQ=="],
29+
"arktype": ["[email protected].4", "", { "dependencies": { "@ark/schema": "0.39.0", "@ark/util": "0.39.0" } }, "sha512-S68rWVDnJauwH7/QCm8zCUM3aTe9Xk6oRihdcc3FSUAtxCo/q1Fwq46JhcwB5Ufv1YStwdQRz+00Y/URlvbhAQ=="],
3030

31-
"bun-types": ["[email protected].0", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-KEaJxyZfbV/c4eyG0vyehDpYmBGreNiQbZIqvVHJwZ4BmeuWlNZ7EAzMN2Zcd7ailmS/tGVW0BgYbGf+lGEpWw=="],
31+
"bun-types": ["[email protected].2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="],
3232

3333
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
3434

3535
"undici-types": ["[email protected]", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
3636

37-
"zod": ["[email protected]", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
37+
"valibot": ["[email protected]", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-bTHNpeeQ403xS7qGHF/tw3EC/zkZOU5VdkfIsmRDu1Sp+BJNTNCm6m5HlwOgyW/03lofP+uQiq3R+Poo9wiCEg=="],
38+
39+
"zod": ["[email protected]", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
3840
}
3941
}

package.json

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
{
2-
"name": "validation-lib-comparaison",
3-
"module": "index.ts",
4-
"type": "module",
2+
"dependencies": {
3+
"arktype": "^2.0.4",
4+
"valibot": "^1.0.0-rc.1",
5+
"zod": "^3.24.2"
6+
},
57
"devDependencies": {
68
"@types/bun": "latest"
79
},
10+
"module": "index.ts",
11+
"name": "validation-lib-comparaison",
812
"peerDependencies": {
9-
"typescript": "^5.0.0"
13+
"typescript": "^5.7.3"
1014
},
11-
"dependencies": {
12-
"arktype": "^2.0.0",
13-
"zod": "^3.24.1"
14-
}
15+
"scripts": {
16+
"bench": "bun bench:bun --warmup 5 --runs 25",
17+
"bench:bun": "hyperfine 'bun src/arktype.ts' 'bun src/valibot.ts' 'bun src/zod.ts'",
18+
"bench:node": "hyperfine --runs 30 'node dist/arktype.js' 'node dist/valibot.js' 'node dist/zod.js'",
19+
"build": "bun scripts/build.ts && echo build success",
20+
"check": "bun check:tsc && bun run build && bun check:once && echo check success",
21+
"check:once": "bun bench:bun --runs 1 && echo check:once success",
22+
"check:tsc": "tsc --noEmit && echo check:tsc success"
23+
},
24+
"type": "module"
1525
}

scripts/build.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Glob } from "bun"
2+
3+
const glob = new Glob("*.ts")
4+
const ignore = ['utils.ts']
5+
const outdir = './dist'
6+
const nameRegex = /dist[\/\\](?<name>[\w-]+)/
7+
8+
function showFileSize (path: string) {
9+
const name = nameRegex.exec(path)?.groups?.name
10+
if (!name) throw new Error(`Failed to extract name from ${path}`)
11+
const stats = Bun.file(path)
12+
const kb = Math.round(stats.size / 1024)
13+
console.log(name.padStart(10), kb, 'KB')
14+
}
15+
16+
function build (file: string) {
17+
const entrypoints = [`./src/${file}`]
18+
Bun.build({ entrypoints, outdir, minify: true }).then((data) => {
19+
showFileSize(data.outputs[0].path)
20+
}).catch((err) => { console.error('Build failed:', err) })
21+
}
22+
23+
for await (const file of glob.scan("./src")) {
24+
if (ignore.includes(file)) continue
25+
build(file)
26+
}

src/arktype.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { type } from "arktype"
2+
import { checkUserA, checkUserB, checkUserC } from './utils'
3+
4+
const userSchema = type({
5+
name: type.string,
6+
age: type.number.default(42),
7+
})
8+
9+
export type User = typeof userSchema.inferOut
10+
11+
export type UserInput = typeof userSchema.inferIn
12+
13+
const userA = userSchema({ name: "Jordan" })
14+
// @ts-expect-error Argument of type '{ name: string; age: number; } | ArkErrors' is not assignable to parameter of type 'User'.
15+
checkUserA(userA)
16+
17+
function createUser (input: UserInput) {
18+
const user = userSchema(input)
19+
if (user instanceof type.errors) return { name: "Jordan", age: 42 } // cannot use userA here
20+
return user
21+
}
22+
23+
const userB = createUser({ name: "Romain", age: 35 })
24+
checkUserB(userB)
25+
26+
// @ts-expect-error age should be a number
27+
const userC = createUser({ name: "Romain", age: "35" })
28+
checkUserC(userC)
29+
30+
console.log('All tests passed successfully.')

src/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
type User = {
2+
name: string,
3+
age?: number,
4+
}
5+
6+
export function checkUserA (userA: User) {
7+
console.assert(userA.age === 42, 'userA.age should be 42')
8+
}
9+
10+
export function checkUserB (userB: User) {
11+
console.assert(userB.age === 35, 'userB.age should be 35')
12+
}
13+
14+
export function checkUserC (userC: User) {
15+
console.assert(userC.name === "Jordan", 'userC.name should be Jordan')
16+
}

src/valibot.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as v from 'valibot'
2+
import { checkUserA, checkUserB, checkUserC } from './utils'
3+
4+
const userSchema = v.object({
5+
name: v.string(),
6+
age: v.optional(v.number(), 42),
7+
})
8+
9+
export type User = v.InferOutput<typeof userSchema>
10+
11+
export type UserInput = v.InferInput<typeof userSchema>
12+
13+
const userA = v.parse(userSchema, { name: "Jordan" })
14+
checkUserA(userA)
15+
16+
function createUser (input: UserInput) {
17+
const result = v.safeParse(userSchema, input)
18+
if (!result.success) return userA
19+
return result.output
20+
}
21+
22+
const userB = createUser({ name: "Romain", age: 35 })
23+
checkUserB(userB)
24+
25+
// @ts-expect-error age should be a number
26+
const userC = createUser({ name: "Romain", age: "35" })
27+
checkUserC(userC)
28+
29+
console.log('All tests passed successfully.')

src/zod.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { z } from "zod"
2+
import { checkUserA, checkUserB, checkUserC } from './utils'
3+
4+
const userSchema = z.object({
5+
name: z.string(),
6+
age: z.number().default(42),
7+
})
8+
9+
export type User = z.infer<typeof userSchema>
10+
11+
export type UserInput = z.input<typeof userSchema>
12+
13+
const userA = userSchema.parse({ name: "Jordan" })
14+
checkUserA(userA)
15+
16+
function createUser (input: UserInput) {
17+
const result = userSchema.safeParse(input)
18+
if (!result.success) return userA
19+
return result.data
20+
}
21+
22+
const userB = createUser({ name: "Romain", age: 35 })
23+
checkUserB(userB)
24+
25+
// @ts-expect-error age should be a number
26+
const userC = createUser({ name: "Romain", age: "35" })
27+
checkUserC(userC)
28+
29+
console.log('All tests passed successfully.')

0 commit comments

Comments
 (0)