Doubter
🤔 Runtime validation and transformation library.
Install / Use
/learn @smikhalevski/DoubterREADME
Runtime validation and transformation library.
- TypeScript first.
- Sync and async validation and transformation flows.
- Circular object references support.
- Collect all validation issues, or exit early.
- Runtime type introspection.
- Human-oriented type coercion.
- High performance and low memory consumption.
- Zero dependencies.
- Pluggable architecture.
- Compatible with Standard Schema.
- Tree-shakable. 3 — 12 kB gzipped depending on what features you use.
- Check out the Cookbook for real-life examples!
npm install --save-prod doubter
<br>
<!--/ARTICLE-->
<!--TOC-->
<span class="toc-icon">🔰 </span>Features
- Introduction
- Validation errors
- Operations
- Conversions
- Early return
- Annotations and metadata
- Parsing context
- Shape piping
- Replace, allow, and deny a value
- Optional and non-optional
- Nullable and nullish
- Exclude a shape
- Deep partial
- Fallback value
- Branded types
- Type coercion
- Introspection
- Localization
- Plugins
- Advanced shapes
<span class="toc-icon">⏱ </span>Performance
<span class="toc-icon">🍿 </span>Comparison with peers
<span class="toc-icon">🧩 </span>Data types
-
Strings<br>
string -
Symbols<br>
symbol -
Objects<br>
objectrecordinstanceOf -
Dates<br>
date -
Promises<br>
promise -
Composition<br>
unionorintersectionandnot
<span class="toc-icon">🍪 </span>Cookbook
- Type-safe URL query params
- Type-safe environment variables
- Type-safe CLI arguments
- Type-safe
localStorage - Rename object keys
- Conditionally applied shapes
Introduction
Let's create a simple shape of a user:
import * as d from 'doubter';
const userShape = d.object({
name: d.string(),
age: d.number(),
});
// ⮕ Shape<{ name: string, age: number }>
This is the shape of an object with two required properties "name" and "age". Shapes are the core concept in Doubter, they are validation and transformation pipelines that have an input and an output.
Apply the shape to an input value with the parse method:
userShape.parse({
name: 'John Belushi',
age: 30,
});
// ⮕ { name: 'John Belushi', age: 30 }
If the provided value is valid, then it is returned as is. If an incorrect value is provided, then a validation error is thrown:
userShape.parse({
name: 'Peter Parker',
age: 'seventeen',
});
// ❌ ValidationError: type.number at /age: Must be a number
Currently, the only constraint applied to the "age" property value is that it must be a number. Let's modify the shape to check that age is an integer and that user is an adult:
const userShape = d.object({
name: d.string(),
- age: d.number()
+ age: d.number().int().between(18, 100)
});
Here we added two operations to the number shape. Operations can check, refine, and alter input values. There are lots of operations available through plugins, and you can easily add your own operation when you need a custom logic.
Now shape would not only check that the "age" is a number, but also assert that it is an integer between 18 and 100:
userShape.parse({
name: 'Peter Parker',
age: 16,
});
// ❌ ValidationError: number.gte at /age: Must be greater than or equal to 18
If you are using TypeScript, you can infer the type of the value that the shape describes:
type User = d.Input<typeof userShape>;
const user: User = {
name: 'Dan Aykroyd',
age: 27,
};
Read more about static type inference and runtime type introspection.
Async shapes
Most of the shapes are synchronous, but they may become asynchronous when one of the below is used:
- Async operations;
- Async conversions;
d.promisethat constrains the fulfilled value;- Custom async shapes.
Let's have a look at a shape that synchronously checks that an input value is a string:
const shape1 = d.string();
// ⮕ Shape<string>
shape1.isAsync; // ⮕ false
If we add an async operation to the string shape, it would become asynchronous:
const shape2 = d.string().checkAsync(value => doAsyncCheck(value));
// ⮕ Shape<string>
shape2.isAsync; // ⮕ true
The shape that checks that the input value is a Promise instance is synchronous, because it doesn't have to wait for
the input promise to be fulfilled before ensuring that input has a proper type:
const shape3 = d.promise();
// ⮕ Shape<Promise<any>>
shape3.isAsync; // ⮕ false
But if you want to check that a promise is fulfilled with a number, here when the shape becomes asynchronous:
const shape4 = d.promise(d.number());
// ⮕ Shape<Promise<number>>
shape4.isAsync; // ⮕ true
Asynchronous shapes don't support synchronous parsing, and would throw an error if it is used:
shape4.parse(Promise.resolve(42));
// ❌ Error: Shape is async
shape4.parseAsync(Promise.resolve(42));
// ⮕ Promise { 42 }
On the other hand, synchronous shapes support asynchronous parsing:
d.string().parseAsync('Mars');
// ⮕ Promise { 'Mars' }
The shape that depends on an asynchronous shape, also becomes asynchronous:
const userShape = d.object({
avatar: d.promise(d.instanceOf(Blob)),
});
// ⮕ Shape<{ avatar: Promise<Blob> }>
userShape.isAsync; // ⮕ true
Parsing and trying
All shapes can parse input values and there are several methods for that purpose. Consider a number shape:
const shape1 = d.number();
// ⮕ Shape<number>
The parse method takes an input value and
returns an output value, or throws a validation error if parsing fails:
shape.parse(42);
// ⮕ 42
shape.parse('Mars');
// ❌ ValidationError: type.number at /: Must be a number
It isn't always convenient to write a try-catch blocks to handle validation errors. Use the
try method in such cases:
shape.try(42);
// ⮕ { ok: true, value: 42 }
shape.try('Mars');
// ⮕ { ok: false, issues: [ … ] }
Read more about issues in Validation errors section.
Sometimes you don't care about validation errors, and want a default value to be returned if things go south. Use the
parseOrDefault method for that:
shape.parseOrDefault(42);
// ⮕ 42
shape.parseOrDefault('Mars');
// ⮕ undefined
shape.parseOrDefault('Pluto', 5.3361);
// ⮕ 5.3361
If you need a fallback value for a nested shape consider using the catch method.
For asynchronous shapes there's an alternative for each of those methods:
parseAsync,
tryAsync, and
parseOrDefaultAsync.
Methods listed in this section can be safely detached from the shape instance:
const { parseOrDefault } = d.string();
parseOrDefault('Jill');
//
