Easy and type-safe validation
- Planned out to be used and shared between backend and frontend
- Powerful type inference: no need to write types manually. Even Discriminated Union types are guessed from your schemas.
- Example of a Discriminated Union Type:
type UnionType = | { discriminator: "has-a"; a: string; } | { discriminator: "has-b"; b: boolean; };
- Simple DSL-like API
- No need for try/catches: the final result will always be returned as the transformed input, or a tree of error messages. This way, there is no need to rearrange the flow to accomodate the try/catch, while reminding you to deal with validation errors
- Easy to extend. You can create a new schema just by extending the classes from the ones provided here
- Implementation that is easy to read and change. If you really need so, you can fork this library and change it without much hassle
- API inspired by
yup
andjoi
This package was built in order to fix some shortcomings (specially with type safety) that many validation libraries have. Most validation libraries try to do a lot, and their code starts getting confusing and with a lot of back and forths. As consequence, unsolved Github issues start pilling up, improving or extending the libraries ourselves becomes hard since there are too many flows with some history behind them, and a lot of broad types (like any
or object
) start surfacing and undermining the type safety of a project.
Our take on validation with not-me
is almost to provide you with enough boilerplate for you to build your own validations schemas. This way, it's easy to maintain and improve this package.
$ npm install not-me
Most IDEs and Javascript text editors (like Visual Studio Code) import modules automatically just by starting to write the name of the value you want to use.
Keeping an app's code splitted into small lazy-loaded chunks is a priority in frontend development. Since legacy systems and some bundlers, like React Native's Metro, do not have tree-shaking, this package does not provide a single index.js
import with all the code bundled in it. Instead, you are encouraged to import what you need from within the directories the package has. For example, the schemas are inside the lib/schemas
directory, so if you want to import a schema for an object type, you need to import it like this import { object } from 'not-me/lib/schemas/object/object-schema
This library offers the following basic types for you to build more complex validation schemas:
array(elementsSchema)
boolean()
date()
equals([...allowed values])
- use
as const
for when you want the types to be the exact value literals. Example:equals([2, 'hello'])
validated value will be typed asnumber | string
butequals([2, 'hello'] as const)
validated value will be typed as2 | 'hello'
- use
number()
object({ property: schemaForTheProperty })
objectOf(schemaForAllProperties)
- same asobject()
but for objects whose keys can be any stringstring()
or([...schemas])
- the value is filtered by multiple schemas till one matches. It's the equivalent to an union type
With these basic blocks, you can build more complex validations, by chaining...
test((v) => <condition> ? null : "Error message")
- will validate if your value matches a condition. If it does, returnnull
. If it doesn't match the condition, return astring
with the error message you want to return.transform((v) => <transform input value into any other value>)
- will allow you to modify the input valuerequired()
- sets the schema to rejectundefined
andnull
valuesdefined()
- sets the schema to rejectundefined
valuesnotNull()
- sets the schema to rejectnull
values
Typescript will guide you in the recommended order by which you should chain validations.
If you follow what auto-complete presents to you, you will be fine.
Most of these schemas and their methods (except transform
) have a last parameter that allows you to set a customized error message for when the value fails to meet the conditions.
You can also customize the default error messages by using the DefaultErrorMessagesManager
in error-messages/default-messages/default-error-messages-manager
.
/*
schema will output
{ common: string } & ({ a: "a"; c: number } | { a: "b"; d: boolean })
`as const` statements are needed to infer the literal value (like 'a' | 'b')
instead of a generic value like `string`
*/
const schema = object({
common: equals(["common"]).required(),
a: equals(["a", "b"] as const).required(),
})
.union((v) => {
if (v.a === "a") {
return {
a: equals(["a"] as const).required(),
c: equals([0]).required(),
};
} else {
return {
a: equals(["b"] as const).required(),
d: equals([false]).required(),
};
}
})
.required();
InferType<typeof schema>
: get the output type of a schemaSchema<T>
: dictates that a value is a schema that has an output type ofT
abortEarly
: stop validation when the first invalid field is found.
import { number } from "not-me/lib/schemas/number/number-schema";
export function positiveInteger() {
return number()
.test((n) => {
// Skip nullable values
if (n == null) {
return null;
}
if (Number.isInteger(n)) {
return null;
} else {
return "Not an integer";
}
})
.test((n) => {
// Skip nullable values
if (n == null) {
return null;
}
if (n >= 0) {
return null;
} else {
return "Not a positive number";
}
});
}
When you set up a schema, you're just pilling up filter functions that will test and transform your initial value. These are the types of filters that are called during validation, by this order:
- Type filter will validate if your input is in a specific type (example: a number, an object, an array, etc...)
- Shape filters will validate the fields in your value. This only applies to object and array values
- Test and Transform filters will run basic true or false checks on your value, or transform your value.
- Files to be changed
- .nvmrc
- Dockerfile.dev
- package.json
engine
field@types/node
version
- tsconfig.base.json
- .github/workflows/main.yml and other CI config files
- delete all
node_modules
directories andpackage-lock.json
files - run
npm run install