-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
174 additions
and
69 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,127 +1,232 @@ | ||
![Validator image](https://github.com/the-minimal/validator/blob/main/docs/the-minimal-validator.jpg?raw=true) | ||
|
||
# @the-minimal/validator | ||
Joe was in search of a simple data validation library that he could learn in 15 minutes. | ||
|
||
Minimalist TypeScript data validation library with great focus on size and performance. | ||
He envisioned a tool with minimal blocking time and low CPU/memory overhead. | ||
|
||
## Install | ||
He hoped for something he could easily extend to meet his specific requirements. | ||
|
||
```bash | ||
yarn add @the-minimal/validator | ||
``` | ||
> Are you like Joe? If so, then `Validator` might just be what you're looking for! | ||
## Highlights | ||
## Stubborn opinions | ||
|
||
- Synchronous JSON-oriented validations | ||
- No compilation or code evaluation | ||
- Fully type safe with type inference | ||
- Fully tree-shakeable ~0.8 KB bundle | ||
- Minimal runtime and type-checking overhead | ||
- Fully tested with 100% coverage | ||
- Zero runtime dependencies | ||
`Validator` is pretty stubborn and doesn't want to be a Jack of all trades. | ||
|
||
## Example | ||
As a result it has many opinions that might not sit well with some folks. | ||
|
||
```ts | ||
// 275 bytes | ||
import { object, string, and2, rangeLength, email } from "@the-minimal/validator"; | ||
import type { Infer } from "@the-minimal/validator"; | ||
<details open> | ||
<summary><b>No data transformation</b></summary> | ||
|
||
const login = object({ // (v: unknown) => asserts v is { | ||
email: and2(string, email), // email: string & Email, | ||
password: and2(string, rangeLength(8, 16)), // password: string & RangeLength<8, 16> | ||
}); // } | ||
Focusing solely on data validation allows us to greatly optimize this library. | ||
|
||
login({ email: '', password: '' }); // Error: type | ||
I advocate for tools that excel in a single task. | ||
|
||
login({ // { | ||
email: '[email protected]', // email: string & Email | ||
password: '12345678' // password: string & RangeLength<8, 16> | ||
}); // } | ||
``` | ||
I believe that, in most cases, we should validate data before performing any transformation. | ||
|
||
This approach simplifies and maintains a clear mental model of what data validation is and how it should be implemented in our applications. | ||
|
||
</details> | ||
|
||
<details> | ||
<summary><b>No asynchronous validations</b></summary> | ||
|
||
JSON data types do not require asynchronous validation. | ||
|
||
Avoid introducing side effects within validations. | ||
|
||
Don't do this: | ||
|
||
```ts | ||
// definition | ||
const validate = and([ | ||
string, | ||
minLength(5), | ||
async (v) => { | ||
if(!(await File.exists(v))) { | ||
throw Error("File does not exist"); | ||
} | ||
} | ||
]); | ||
|
||
## FAQ | ||
// endpoint | ||
await validate(filename); | ||
``` | ||
|
||
Do this instead: | ||
|
||
```ts | ||
// definition | ||
const validate = and([ | ||
string, | ||
minLength(5), | ||
]); | ||
|
||
// endpoint | ||
validate(filename); | ||
|
||
if(!(await File.exists(filename))) { | ||
throw Error("File does not exist"); | ||
} | ||
``` | ||
|
||
</details> | ||
|
||
<details> | ||
<summary><b>How do I validate <code>map</code>/<code>set</code>/<code>date</code>/etc.?</b></summary> | ||
<summary><b>No compilation or eval</b></summary> | ||
|
||
The main focus of this library is data validation of JSON. | ||
Compilation with `Function`/`eval` syntax is not allowed in all environments and, more importantly, it would mean maintaining two different runtime implementations, which I do not want. | ||
|
||
JSON doesn't support these data types, so it makes no sense to include them in this library. | ||
It also sacrifices initial blocking for faster subsequent runs, which might be useful in some scenarios. However, this library is primarily designed for serverless runtimes, where this would result in drastically slower performance. | ||
|
||
</details> | ||
|
||
<details> | ||
<summary><b>How do I <code>extend</code>/<code>omit</code>/<code>pick</code> objects?</b></summary> | ||
<summary><b>No methods like <code>extend</code>/<code>omit</code></b></summary> | ||
|
||
In order to allow such functions we'd have to make the schema accessible from the outside. | ||
In order to allow such methods, we would have to make the schema accessible from the outside. | ||
|
||
This would change the design from using individual callable assertions to using objects with properties where one of those properties is the assertion. | ||
This would change the design from using individual callable validations to using objects with properties, one of which is the validation. | ||
|
||
Additionally, this would make it possible to for example extend any object even if we don't want users to extend such an object. | ||
Additionally, this would make it possible, for example, to extend any object, even if we don't want users to have such capability. | ||
|
||
To fix this issue we would have to introduce some form of object schema freezing on top of that. | ||
To address this issue, we would need to introduce some form of object schema freezing. | ||
|
||
All of that complicates the API, makes the library slower and inflates the bundle size. | ||
All of this complicates the API, slows down the library, and increases the bundle size. | ||
|
||
You can make object extendable by exporting its schema separately and then spreading it inside another schema. | ||
You can make an object extendable by exporting its schema separately and then spreading it inside another schema. | ||
|
||
</details> | ||
|
||
<details> | ||
<summary><b>Why is there no <code>null</code>/<code>undefined</code>?</b></summary> | ||
<summary><b>No validations like <code>map</code>/<code>set</code></b></summary> | ||
|
||
Strictly checking only for `null` or `undefined` makes no sense. | ||
The main focus of this library is the data validation of JSON (primarily from fetch requests). | ||
|
||
You always want to know if something can be `something` **OR** `nothing`. | ||
JSON does not support these data types, so it makes no sense to include them in this library. | ||
|
||
So you should always use `nullable`/`optional`/`nullish` instead. | ||
If you want to use this library with these higher-level primitives, then I recommend validating the input of these primitives. | ||
|
||
</details> | ||
|
||
<details> | ||
<summary><b>Why is there no <code>any</code>/<code>unknown</code>?</b></summary> | ||
<summary><b>No validations like <code>any</code>/<code>unknown</code></b></summary> | ||
|
||
You should always define your types otherwise what's the point of using TypeScript and this library? | ||
You should always define concrete types. | ||
|
||
Otherwise, what's the point of using TypeScript together with this library? | ||
|
||
</details> | ||
|
||
|
||
<details> | ||
<summary><b>How do I run validations in <code>async</code>?</b></summary> | ||
<summary><b>No validations like <code>null</code>/<code>undefined</code></b></summary> | ||
|
||
None of the JSON data types need to be validated asynchronously. | ||
Checking strictly for `null` or `undefined` alone makes no sense. | ||
|
||
Validating side effects inside the validations is not a good idea and should be done after the validation is done. | ||
You always want to know if something can be _something_ or _nothing_. | ||
|
||
Don't do this: | ||
Therefore, you should always use `nullable`, `optional`, or `nullish` instead. | ||
|
||
</details> | ||
|
||
## Focused features | ||
|
||
`Validator` is a watchful eye that plagues your editor with errors if you feed it data you haven't agreed upon, but otherwise, it stays quiet as a mouse. | ||
|
||
- Everything is an `Assertion` | ||
- `Assertion`s are simple functions | ||
- `Assertion`s are composed together | ||
- `Assertion`s are type-safe | ||
- `Assertion`s are tree-shakeable | ||
|
||
## Incredible numbers | ||
|
||
`Validator` is an obsessed overachiever who wants to be the smallest and fastest one on the track. | ||
|
||
- 39 `Assertion`s | ||
- 808 bytes bundle | ||
- ~ 5x faster data validation than `Zod` | ||
- ~ 200x less memory consumption than `Zod` | ||
- ~ 50x faster type-checking than `Zod` | ||
- 0 runtime dependencies | ||
- 100% test coverage | ||
|
||
## Simple examples | ||
|
||
<details> | ||
<summary><b>How do I validate types?</b></summary> | ||
|
||
```ts | ||
// definition | ||
const validate = and([ | ||
string, | ||
async (v) => { | ||
if(!(await File.exists(v))) { | ||
error("fileExists", v); | ||
} | ||
} | ||
]); | ||
string("Hello, World!"); | ||
number(420); | ||
boolean(true); | ||
``` | ||
|
||
// endpoint | ||
await validate(body); | ||
</details> | ||
|
||
<details> | ||
<summary><b>How do I validate values?</b></summary> | ||
|
||
```ts | ||
value(26); | ||
notValue(0); | ||
minValue(18); | ||
maxValue(100); | ||
rangeValue(18, 100); | ||
``` | ||
|
||
Do this instead: | ||
</details> | ||
|
||
<details> | ||
<summary><b>How do I validate lenghts?</b></summary> | ||
|
||
```ts | ||
// definition | ||
const validate = string; | ||
length(5); | ||
notLength(0); | ||
minLength(8); | ||
maxLength(16); | ||
rangeLength(8, 16); | ||
``` | ||
|
||
// endpoint | ||
validate(body); | ||
</details> | ||
|
||
if(!(await File.exists(body))) { | ||
throw Error("File does not exist"); | ||
} | ||
<details open> | ||
<summary><b>How do I combine validations?</b></summary> | ||
|
||
```ts | ||
const register = object({ | ||
email: and([string, rangeLength(5, 35), email]), | ||
password: and([string, rangeLength(8, 16)]), | ||
role: union(["ADMIN", "USER"]), | ||
friends: array(string) | ||
}); | ||
|
||
register("Oh not this is gonna throw"); | ||
|
||
register({ | ||
email: "[email protected]", | ||
password: "Test123456", | ||
role: "ADMIN", | ||
friends: ["Joe"] | ||
}); | ||
``` | ||
|
||
</details> | ||
|
||
## Great journey | ||
|
||
Are you really Joe? | ||
|
||
Do you dare to let this demon command you? | ||
|
||
If so, repeat the spell below and good luck on your journey! | ||
|
||
```bash | ||
yarn add @the-minimal/validator | ||
``` | ||
|
||
## Notes | ||
|
||
- All reported sizes are for minified and gzipped code | ||
- Reproducible and highly detailed benchmarks are on the way | ||
- `Validation` and `Assertion` are the same things |