ts-decoders
v1.1.0
Published
A typescript validation library.
Downloads
477
Maintainers
Readme
ts-decoders (demo)
This module facilitates validating/decoding an unknown
input and returning a properly typed response (on success), or returning useful errors. It provides an assortment of decoder primatives which are usable as-is, easily customized, and composable with other decoders (including any custom decoders you may make).
Example usage:
import { assert } from 'ts-decoders';
import { numberD } from 'ts-decoders/decoders';
// assert() is a convenience function which wraps a decoder
const numberV = assert(numberD());
const value1 = numberV(1); // returns 1
const value2 = numberV('1'); // throws AssertDecoderError
Example of composing multiple decoders together to validate a complex argument:
import { objectD, numberD } from 'ts-decoders/decoders';
const myDecoder = objectD({ count: numberD() });
// returns DecoderSuccess
const result = myDecoder.decode({ count: 1 });
const value = result.value; // { count: 1 }
// returns DecoderError
// message = 'invalid value for key ["count"] > must be a number'
myDecoder.decode({ count: '1' });
Installation
yarn add ts-decoders
# or
npm install --save ts-decoders
Why this module is useful
ts-decoders
greatly simplifies validation/decoding, allowing you to build up complex validation/decoding functions from composible parts. If you aren't familiar, there are number of decoder libraries available for typescript (see Similar projects). This library differentiates itself via:
- Fast and flexible method for creating custom error messages (see Working with errors).
- The ability to return the first error or an array of all the errors.
- Full support for asyncronous decoders (see Working with promises).
- Class based
Decoder
interface with the ability to specify the decoder's expected input type in addition to it's return type (see Interfaces).
Usage
- Demo
- Basic usage
- Working with errors
- Creating custom decoders
- Working with promises
- Interfaces
- Tips and tricks
- Decoder API
- Similar projects
Basic usage
This module exports an assortment of primative decoder functions which each return a decoder. By convention, all of the exported decoder functions have the suffix D
. For example, the numberD()
function returns a Decoder<number, any>
suitable for checking if an unknown value is a number
.
Other decoders are customizable. For example, the exactlyD()
decoder accepts an argument and decodes inputs to make sure they are ===
to that argument.
import { areDecoderErrors } from 'ts-decoders';
import { exactlyD } from 'ts-decoders/decoders';
const result = exactlyD('one').decode('one'); // returns a DecoderSuccess<"one">
const result = exactlyD('two').decode('one'); // returns (not throws) a DecoderError
if (areDecoderErrors(result)) {
// result is type `DecoderError[]`
// do stuff...
} else {
// result is type `DecoderSuccess<"one">`
// value is type `"one"`
const value = result.value;
}
For your convenience, you can wrap any decoder with the assert
function to create a valdiation function which returns valid values directly (instead of returning DecoderSuccess
values) or which throws (not returns) an AssertDecoderError
.
import { assert } from 'ts-decoders';
import { numberD } from 'ts-decoders/decoders';
const myNumberValidator = assert(numberD());
const value = myNumberValidator(1); // returns 1
const value = myNumberValidator('1'); // will throw (not return) a AssertDecoderError
Many decoder functions aid with composing multiple decoders together.
For example, the undefinableD()
decoder accepts another decoder as an argument and successfully decoders either undefined
or whatever input(s) the other decoder accepts.
Here we pass numberD()
to undefinableD()
to create a decoder that accepts either number
or undefined
inputs.
const myNumberDecoder = undefinableD(numberD());
const result = myNumberDecoder.decode(1); // returns a DecoderSuccess<number | undefined>
const result = myNumberDecoder.decode(undefined); // returns a DecoderSuccess<number | undefined>
const result = myNumberDecoder.decode('1'); // returns DecoderError[]
A more complex example of decoder composition is the objectD()
decoder function which is useful for decoding and properly typing JSON data. The objectD()
decoder receives an object argument which is used to decode inputs and validate that they match the shape of the object argument. The objectD()
decoder verifies that the provided value is a non-null object, that the object has the specified keys, and that the values of the object's keys pass the provided decoder checks.
const myObjectDecoder = objectD({
payload: objectD({
values: arrayD(nullableD(numberD())),
}),
});
const goodInput = {
payload: {
values: [0, null, 2],
},
};
const success = myObjectDecoder.decode(goodInput); // will return
// `DecoderSuccess<{ payload: string; { values: Array<number | null> } }>`
const badInput = {
payload: {
// note the string value included in the array
values: [0, null, '1'],
},
};
const errors = myObjectDecoder.decode(badInput); // will return `DecoderError[]`
errors.get(0)!.message;
// invalid value for key ["payload"]
// > invalid value for key ["values"]
// > invalid element [2]
// > must be a number or must be null
Working with errors
also see the DecoderError
interface
One of the strengths of ts-decoders is it's ability to create useful, human and machine readable error messages.
The errorMsg option
see the DecoderErrorMsgArg
type
If you wish to customize the error message(s) a decoder returns, each decoder function allows providing an errorMsg
option. This errorMsg
option provides a few choices for customizing the error messages returned by that decoder.
1. Providing an errorMsg
string
You can pass a string as the errorMsg
option. That string will then be used as the error message whenever that decoder receives an input that results in one or more DecoderError[]
.
Example:
const myObjectDecoder = objectD({
payload: objectD({
values: arrayD(nullableD(numberD()), { errorMsg: 'very bad array!' }),
}),
});
const badInput = {
payload: {
values: [0, null, '1'],
},
};
const errors = myObjectDecoder.decode(badInput); // will return `DecoderError[]`
errors[0].message;
// "invalid value for key \"payload\"
// > invalid value for key \"values\"
// > very bad array!"
errors[0].location; // "payload.values"
errors[0].path(); // ["payload", "values"]
errors[0].child.message;
// "invalid value for key \"values\"
// > very bad array!"
2. Providing an errorMsg
factory function
For more control over your error messages, you can provide a (error: DecoderError[]) => DecoderError | DecoderError[]
function as the errorMsg
option.
If one or more DecoderError
occur, the errors will be passed to the provided errorMsg
function where you can either manipulate the errors or return new errors. Your function must return at least one DecoderError.
Example:
const errorMsgFn = (input: any, errors: DecoderError[]) => {
errors.forEach(error => {
const { decoderName } = error.child;
if (decoderName !== 'arrayD') {
error.message = 'array must have a length of 2';
} else if (error.child.child) {
error.message = 'must be an array of numbers';
} else {
error.message = 'must be an array';
}
});
return errors;
};
// The `chainOfD` decoder receives a spread of decoder functions
// and calls each of them sequentially, passing the successful output
// of one decoder as input to the next decoder or immediately
// returning in the case of a DecoderError
const LAT_LONG_DEC = chainOfD(
arrayD(numberD()),
predicateD(input => input.length === 2),
{ decoderName: 'latLongDecoder', errorMsg: errorMsgFn },
);
const errors = LAT_LONG_DEC.decode([1]);
errors[0].message; // "array must have a length of 2"
allErrors option
By default, (most) decoders will immediately return the first error they encounter. If you pass the allErrors: true"
option when calling a decoder function that supports it, then the returned decoder will instead process and return all errors from an input value (or DecoderSuccess
). These errors will be returned as an array.
Example:
const myDecoder = arrayD(stringD(), { allErrors: true });
const errors = myDecoder.decode([1, 'null', null, 'two']);
errors[0].message;
// "invalid value for key [0]
// > must be a string"
errors[1].message;
// "invalid value for key [2]
// > must be a string"
decoderName option
DecoderError
objects have an optional decoderName: string
property which can be useful for easily identifying what decoder an error came from. All of the decoder functions in this module add a decoderName
to their error messages. By passing the decoderName: string
option when calling a decoder function, you can change the decoderName
associated with a decoder's errors.
Example:
const LAT_LONG_DEC = chainOfD(
arrayD(numberD()),
predicateD(input => input.length === 2),
{ decoderName: 'latLongDecoder' },
);
Creating custom decoders
There are a two ways of creating custom decoders.
1. Decoder composition
Composing multple decoders together is the simplest way is to create a more complex decoder. For example, the following latitude and longitude decoder is created by composing arrayD(numberD())
and predicateD(input => input.length === 2)
using chainOfD()
. The chainOfD
decoder receives a spread of decoder functions and calls each of them sequentially, passing the successful output of one decoder as input to the next decoder or immediately returning in the case of a DecoderError.
const LAT_LONG_DEC = chainOfD(
arrayD(numberD()),
predicateD(input => input.length === 2),
{ decoderName: 'latLongDecoder' },
);
2. Creating a custom decoder from scratch
For more flexibility, you can create a new decoder from scratch using either the Decoder
or AsyncDecoder
constructors (see the working with promises section for a description of the differences between Decoder
and AsyncDecoder
). To make a new decoder from scratch, simply pass a custom decode function to the Decoder
constructor. A decode function is a function which receives a value and returns a DecodeSuccess
object on success or a DecoderError | DecoderError[]
on failure.
Example:
const myCustomDecoder = new Decoder(input =>
typeof input === 'boolean'
? new DecoderSuccess(input)
: new DecoderError(input, 'invalid type', 'must be a boolean'),
);
// You can then compose this decoder with others normally
objectD({ likesTsDecoders: myCustomDecoder });
// Or use it directly
myCustomDecoder.decode(true);
Specifying an input value type
While most (all?) of the decoders shipped with this library expect an unknown input value, it is possible to create a decoder which requires an already typed input value. The I
type arg in Decoder<R, I>
is the input variable type (the default is any
). To create a decoder which requires an input value to be of a specific type, simply type the input of the decoder's decodeFn.
Example:
const arrayLengthDecoder = new Decoder((input: any[]) =>
input.length < 100
? new DecoderSuccess(input)
: new DecoderError(
input,
'invalid length',
'must have length less than 100',
),
);
arrayLengthDecoder.decode(1); // type error! decode() expects an array
This decoder only works on array values. One use case for a decoder like this is inside the chainOfD()
decoder, after we have already verified that a value is an array.
Example:
chainOfD(
arrayD(),
arrayLengthDecoder, // <-- this will only be called when the value is an array
);
Conventions for creating custom decoder functions
Like this module, you may wish to create custom decoder functions (e.g. objectD()
) to dynamically compose decoders together or to help create new decoders. It's recommended that, before doing so, you familiarize yourself with the conventions used by this module.
- If your function allows users to pass options to it, in general those options should all go into an optional options object which is the last argument to the function.
- An exception to this recommendation would be the
dictionaryD()
function, which can accept an optional key decoder as the second argument and an options object as the third argument. In this case, typescript overloads are used to keep the API friendly.
- An exception to this recommendation would be the
- If appropriate, allow users to customize the returned errors by passing a
errorMsg?: DecoderErrorMsgArg
option. - If your decoder may return multiple
DecoderError
, immediately return the first error by default. Allow users to pass anallErrors: true
option to return multiple errors. - If your function takes one or more decoders as an argument, you need to handle the possibility of being passed a
AsyncDecoder
. If you receive one or moreAsyncDecoders
, your composition function should return aAsyncDecoder
. Typescript overloads can be used to properly type the different returns. - This module exports various utilities that can simplify the process of creating custom decoder functions.
Working with promises
Every decoder supports calling its decode()
method with a promise which returns the value to be decoded. In this scenerio, decode()
will return a Promise<DecoderResult<T>>
. Internally, the decoder will wait for the promise to resolve before passing the value to its decodeFn
. As such, the internal decodeFn
will never be passed a promise value.
If you wish to create a custom decoder with a decodeFn
which returns a promise, then you must use the AsyncDecoder
class. AsyncDecoder
is largely identical to Decoder
, except its decode()
method always returns Promise<DecoderResult<T>>
(not just when called with a promise value) and it's decodeFn
returns a promise. Additionally, when calling a decoder function with a AsyncDecoder
and allErrors: true
arguments, many decoder functions will process input values in parallel rather than serially.
As an example, calling objectD()
or arrayD()
with a AsyncDecoder
and allErrors: true
will create a decoder which decodes each key in parallel.
declare class PersonService {
checkIfIdExists(id: string): Promise<boolean>;
}
declare const personService: PersonService;
const personIdDecoder = chainOfD(
uuidD(),
new AsyncDecoder(async id => {
const result = await personService.checkIfIdExists(id);
if (result) return new DecoderSuccess(id);
return new DecoderError(id, 'invalid id', 'the provided id does not exist');
}),
);
const myObjectDecoder = objectD(
{
type: stringD(),
payload: objectD({
values: arrayD(nullableD(personIdDecoder)),
}),
},
{ allErrors: true },
);
// when you compose Decoders with a AsyncDecoder,
// the result is a AsyncDecoder
myObjectDecoder instanceof AsyncDecoder === true;
Interfaces
This module exports two base decoder classes Decoder<R, I>
and AsyncDecoder<R, I>
. It also exports a base DecoderSuccess<T>
class and DecoderError
class.
Decoder<R, I>
The first type argument, R
, contains the successful return type of the decoder. The second type argument, I
, contains the type of the input argument passed to the decoder.
class Decoder<R, I = any> {
/** The internal function this decoder uses to decode values */
decodeFn: (value: I) => DecodeFnResult<R>;
constructor(decodeFn: (value: I) => DecodeFnResult<R>): Decoder<R, I>;
/**
* Decodes a value of type `Promise<I>` and returns
* a `Promise<DecoderResult<R>>`.
*/
decode(value: Promise<I>): Promise<DecoderResult<R>>;
/**
* Decodes a value of type `I` and returns a `DecoderResult<R>`.
*/
decode(value: I): DecoderResult<R>;
/**
* On decode failure, handle the DecoderErrors.
*/
catch<K>(
fn: (input: unknown, errors: DecoderError[]) => DecodeFnResult<K>,
): Decoder<K | R, I>;
/**
* On decode success, transform a value using a provided transformation function.
*/
map<K>(fn: (value: R) => K): Decoder<K, I>;
/**
* On decode success, perform a new validation check.
*/
chain<K>(fn: (input: R) => DecodeFnResult<K> | Decoder<K, R>): Decoder<K, I>;
chain<K>(decoder: Decoder<K, R>): Decoder<K, I>;
toAsyncDecoder(): AsyncDecoder<R, I>;
}
AsyncDecoder<R, I>
The first type argument, R
, contains the successful return type of the decoder. The second type argument, I
, contains the type of the input argument passed to the decoder.
class AsyncDecoder<R, I = any> {
/** The internal function this decoder uses to decode values */
readonly decodeFn: (input: I) => Promise<DecodeFnResult<R>>;
constructor(
decodeFn: (value: I) => Promise<DecodeFnResult<R>>,
): AsyncDecoder<R, I>;
/**
* Decodes a value (or promise returning a value) of type `I`
* and returns a `Promise<DecoderResult<R>>`
*/
decode(value: I | Promise<I>): Promise<DecoderResult<R>>;
/**
* On decode failure, handle the DecoderErrors.
*/
catch<K>(
fn: (
input: unknown,
errors: DecoderError[],
) => DecodeFnResult<K> | Promise<DecodeFnResult<K>>,
): AsyncDecoder<K | R, I>;
/**
* On decode success, transform a value using a provided transformation function.
*/
map<K>(fn: (value: R) => K | Promise<K>): AsyncDecoder<K, I>;
/**
* On decode success, perform a new validation check.
*/
chain<K>(
fn: (input: R) => DecodeFnResult<K> | Promise<DecodeFnResult<K>>,
): AsyncDecoder<K, I>;
chain<K>(decoder: Decoder<K, R> | AsyncDecoder<K, R>): AsyncDecoder<K, I>;
}
DecoderSuccess
class DecoderSuccess<T> {
constructor(value: T): DecoderSuccess<T>;
value: T;
}
DecoderError
class DecoderError {
/** The input that failed validation. */
input: any;
/** The type of error. */
type: string;
/** A human readable error message. */
message: string;
/** The name of the decoder which created this error. */
decoderName: string;
/**
* A human readable string showing the nested location of the error.
* If the validation error is not nested, location will equal a blank string.
*/
location: string;
/** The `DecoderError` which triggered this `DecoderError`, if any */
child?: DecoderError;
/**
* The key associated with this `DecoderError` if any.
*
* - example: this could be the index of the array element which
* failed validation.
*/
key?: any;
/** Convenience property for storing arbitrary data. */
data: any;
constructor(
input: any,
type: string,
message: string,
options?: {
decoderName?: string;
location?: string;
child?: DecoderError;
key?: any;
data?: any;
},
): DecoderError;
/**
* Starting with this error, an array of the keys associated with
* this error as well as all child errors.
*/
path(): any[];
}
DecoderResult
type DecoderResult<T> = DecoderSuccess<T> | DecoderError[];
DecodeFnResult
type DecodeFnResult<T> = DecoderSuccess<T> | DecoderError | DecoderError[];
DecoderErrorMsgArg
type DecoderErrorMsgArg =
| string
| ((input: any, errors: DecoderError[]) => DecoderError | DecoderError[]);
Tips and tricks
The assert() function
It may be the case that you simply want to return the validated value from a decoder directly, rather than a DecoderResult
. In this case, wrap a decoder with assert()
to get a callable function which will return a valid value on success, or throw (not return) a AssertDecoderError
on failure.
Example:
const validator = assert(numberD());
const value = validator(1); // returns 1
const value = validator('1'); // will throw a `AssertDecoderError`
The decoder map() method
Decoders have a map
method which can be used to transform valid input values. For example, say you are receiving a date param in the form of a string, and you want to convert it to a javascript Date
object. The following decoder will verify that a string is in a YYYY-MM-DD
format and, if so, convert the string to a date.
const stringDateDecoder =
// this regex verifies that a string is of the form `YYYY-MM-DD`
matchD(/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/).map(
value => new Date(value),
);
const result = stringDateDecoder.decode('2000-01-01'); // returns `DecoderSuccess<Date>`
if (result instanceof DecoderSuccess) {
const value: Date = result.value;
}
The decoder chain() method
also see the chainOfD
decoder for similar functionality
Decoders have a chain
method which can be used to chain on an additional validation check if the first one was a success. Building off the example for map
above, say you've made a stringDateDecoder
which recieves a string and converts it to a javascript Date
object if the string has the form YYYY-MM-DD
. You decide that you want to perform an additional validation check on the Date
object to make sure that the date is after the year 2000.
const afterYear2000Decoder = stringDateDecoder.chain(date => {
if (date.valueOf() > new Date(2000)) {
return new DecoderSuccess(date);
}
return new DecoderError(date, 'invalid-date', 'must be after the year 2000');
});
const result = afterYear2000Decoder.decode('1998-01-01'); // returns `[DecoderError]`
Getting a type equal to the return type of a decoder
For convenience, you may want to generate a typescript type which is equal to the return type of a decoder. For example, if you create a decoder for some web request params, you might also like to separately have a type representing these web request params that you can use in type assertions. You can use the DecoderReturnType<T>
and DecoderInputType<T>
types for this.
For example:
const textDec = chainOfD(
stringD(),
predicateD(input => input.length > 1, {
errorMsg: 'must be 2 or more characters',
}),
);
const paramsDec = objectD({
__typename: exactlyD('Person'),
id: uuidD(),
firstName: textDec,
middleName: textDec,
lastName: textDec,
address: objectD({
__typename: exactlyD('Address'),
street: textDec,
city: textDec,
}),
});
type Params = DecoderReturnType<typeof paramsDec>;
// here, the Params type is equal to
//
// interface Params {
// __typename: "Person",
// id: string,
// firstName: string,
// middleName: string,
// lastName: string,
// address: {
// __typename: "Address",
// street: string,
// city: string,
// }
// }
Decoder API
assert()
assert()
accepts a single decoder as an argument and returns a new function which can be used to decode the same values as the provided decoder. On decode success, the validated value is returned directly and on failure the AssertDecoderError
is thrown (rather than returned).
Interface:
class AssertDecoderError extends Error {
errors: DecoderError[];
constructor(errors: DecoderError[]): AssertDecoderError;
}
function assert<R, V>(
decoder: Decoder<R, V>,
): { (value: V): R; (value: Promise<V>): Promise<R> };
function assert<R, V>(
decoder: AsyncDecoder<R, V>,
): (value: V | Promise<V>) => Promise<R>;
Example:
const validator = assert(numberD());
const value = validator(1); // returns 1;
const value = validator('1'); // will throw a `AssertDecoderError`
anyD()
anyD()
creates a decoder which always returns DecoderSuccess
with whatever input value is provided to it.
Interface:
function anyD<T = any>(): Decoder<T, any>;
Example:
// Decoder<any>;
const decoder1 = anyD();
// Decoder<string>;
const decoder2 = anyD<string>();
anyOfD()
anyOfD()
accepts an array of decoders and attempts to decode a provided value using each of them, in order, returning the first successful result or DecoderError[]
if all fail. Unlike other decoder functions, anyOfD()
always returns all errors surfaced by it's decoder arguments. By default, decoder arguments are tried in the order they are given.
Async: when calling anyOfD()
with one or more AsyncDecoder
arguments, you can pass a decodeInParallel: true
option to specify that a provided value should be tried against all decoder arguments in parallel.
Interface:
interface AnyOfDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
decodeInParallel?: boolean;
}
function anyOfD<T extends Decoder<unknown>>(
decoders: T[],
options?: AnyOfDecoderOptions,
): Decoder<DecoderReturnType<T>>;
function anyOfD<T extends Decoder<unknown> | AsyncDecoder<unknown>>(
decoders: T[],
options?: AnyOfDecoderOptions,
): AsyncDecoder<DecoderReturnType<T>>;
Example:
// Decoder<string | number>;
const decoder = anyOfD(stringD(), numberD());
arrayD()
arrayD()
can be used to make sure an input is an array. If an optional decoder argument is provided, that decoder will be used to process all of the input's elements.
Options:
- If you pass an
allErrors: true
option as well as any AsyncDecoders as arguments, thenarrayD()
will create a new AsyncDecoder which decodes each index of the input array in parallel.
Related:
Interface:
interface ArrayDecoderOptions {
decoderName?: string;
allErrors?: boolean;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function arrayD<R = any>(options?: ArrayDecoderOptions): Decoder<R[]>;
function arrayD<R>(
decoder: Decoder<R>,
options?: ArrayDecoderOptions,
): Decoder<R[]>;
function arrayD<R>(
decoder: AsyncDecoder<R>,
options?: ArrayDecoderOptions,
): AsyncDecoder<R[]>;
Example:
// Decoder<string[]>;
const decoder = arrayD(stringD());
booleanD()
booleanD()
can be used to verify that an unknown value is a boolean
.
Interface:
interface BooleanDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function booleanD(options?: BooleanDecoderOptions): Decoder<boolean, any>;
Example:
// Decoder<boolean>;
const decoder = booleanD();
chainOfD()
chainOfD()
accepts a spread or array of decoders and attempts to decode a provided value using all of them, in order. The successful output of one decoder is provided as input to the next decoder. chainOfD()
returns the DecoderSuccess
value of the last decoder in the chain or DecoderError
on the first failure. Alternate names for this decoder could be: pipe, compose, or allOf.
Related:
Interface:
interface ChainOfDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
export function chainOfD<A, B, C, D, E, F, G>(
a: Decoder<B, A>,
b: Decoder<C, B>,
c: Decoder<D, C>,
d: Decoder<E, D>,
e: Decoder<F, E>,
f: Decoder<G, F>,
options?: ChainOfDecoderOptions,
): Decoder<G, A>;
export function chainOfD<A, B, C, D, E, F>(
a: Decoder<B, A>,
b: Decoder<C, B>,
c: Decoder<D, C>,
d: Decoder<E, D>,
e: Decoder<F, E>,
options?: ChainOfDecoderOptions,
): Decoder<F, A>;
export function chainOfD<A, B, C, D, E>(
a: Decoder<B, A>,
b: Decoder<C, B>,
c: Decoder<D, C>,
d: Decoder<E, D>,
options?: ChainOfDecoderOptions,
): Decoder<E, A>;
export function chainOfD<A, B, C, D>(
a: Decoder<B, A>,
b: Decoder<C, B>,
c: Decoder<D, C>,
options?: ChainOfDecoderOptions,
): Decoder<D, A>;
export function chainOfD<A, B, C>(
a: Decoder<B, A>,
b: Decoder<C, B>,
options?: ChainOfDecoderOptions,
): Decoder<C, A>;
export function chainOfD<A, B>(
a: Decoder<B, A>,
options?: ChainOfDecoderOptions,
): Decoder<B, A>;
export function chainOfD<A, B = any>(
decoders: [Decoder<any, A>, ...Array<Decoder<any>>],
options?: ChainOfDecoderOptions,
): Decoder<B, A>;
export function chainOfD<A, B, C, D, E, F, G>(
a: Decoder<B, A> | AsyncDecoder<B, A>,
b: Decoder<C, B> | AsyncDecoder<C, B>,
c: Decoder<D, C> | AsyncDecoder<D, C>,
d: Decoder<E, D> | AsyncDecoder<E, D>,
e: Decoder<F, E> | AsyncDecoder<F, E>,
f: Decoder<G, F> | AsyncDecoder<G, F>,
options?: ChainOfDecoderOptions,
): AsyncDecoder<G, A>;
export function chainOfD<A, B, C, D, E, F>(
a: Decoder<B, A> | AsyncDecoder<B, A>,
b: Decoder<C, B> | AsyncDecoder<C, B>,
c: Decoder<D, C> | AsyncDecoder<D, C>,
d: Decoder<E, D> | AsyncDecoder<E, D>,
e: Decoder<F, E> | AsyncDecoder<F, E>,
options?: ChainOfDecoderOptions,
): AsyncDecoder<F, A>;
export function chainOfD<A, B, C, D, E>(
a: Decoder<B, A> | AsyncDecoder<B, A>,
b: Decoder<C, B> | AsyncDecoder<C, B>,
c: Decoder<D, C> | AsyncDecoder<D, C>,
d: Decoder<E, D> | AsyncDecoder<E, D>,
options?: ChainOfDecoderOptions,
): AsyncDecoder<E, A>;
export function chainOfD<A, B, C, D>(
a: Decoder<B, A> | AsyncDecoder<B, A>,
b: Decoder<C, B> | AsyncDecoder<C, B>,
c: Decoder<D, C> | AsyncDecoder<D, C>,
options?: ChainOfDecoderOptions,
): AsyncDecoder<D, A>;
export function chainOfD<A, B, C>(
a: Decoder<B, A> | AsyncDecoder<B, A>,
b: Decoder<C, B> | AsyncDecoder<C, B>,
options?: ChainOfDecoderOptions,
): AsyncDecoder<C, A>;
export function chainOfD<A, B>(
a: Decoder<B, A> | AsyncDecoder<B, A>,
options?: ChainOfDecoderOptions,
): AsyncDecoder<B, A>;
export function chainOfD<A, B = any>(
decoders: [
Decoder<any, A> | AsyncDecoder<any, A>,
...Array<Decoder<any> | AsyncDecoder<any>>,
],
options?: ChainOfDecoderOptions,
): AsyncDecoder<B, A>;
Example:
// Decoder<string[]>;
const decoder = chainOfD(
arrayD(stringD()),
predicateD(input => input.length === 2),
);
constantD()
constantD()
accepts a value: T
argument and creates a decoder which always returns DecoderSuccess<T>
with the provided value
argument, ignoring its input.
Interface:
function constantD<T extends string | number | bigint | boolean>(
exact: T,
): Decoder<T, any>;
function constantD<T>(value: T): Decoder<T, any>;
Example:
// Decoder<number>;
const decoder = constantD(13);
dictionaryD()
dictionaryD()
receives a decoder argument and uses that decoder to process all values (regardless of key) of an input object. You can pass an optional key decoder as the second argument which will be used to decode each key of an input object.
Options:
- If you pass an
allErrors: true
option as well as any AsyncDecoders as arguments, thendictionaryD()
will create a new AsyncDecoder which decodes each key of the input object in parallel.
Related:
Interface:
interface DictionaryDecoderOptions {
decoderName?: string;
allErrors?: boolean;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function dictionaryD<R, V>(
valueDecoder: Decoder<R, V>,
options?: DictionaryDecoderOptions,
): Decoder<{ [key: string]: R }, V>;
function dictionaryD<R, V>(
valueDecoder: Decoder<R, V>,
keyDecoder: Decoder<string, string>,
options?: DictionaryDecoderOptions,
): Decoder<{ [key: string]: R }, V>;
function dictionaryD<R, V>(
decoder: AsyncDecoder<R, V>,
options?: DictionaryDecoderOptions,
): AsyncDecoder<{ [key: string]: R }, V>;
function dictionaryD<R, V>(
valueDecoder: Decoder<R, V> | AsyncDecoder<R, V>,
keyDecoder: Decoder<string, string> | AsyncDecoder<string, string>,
options?: DictionaryDecoderOptions,
): AsyncDecoder<{ [key: string]: R }, V>;
Example:
// Decoder<{[key: string]: string}>;
const decoder1 = dictionaryD(stringD());
// Decoder<{[key: string]: number}>;
const decoder2 = dictionaryD(numberD(), predicateD(input => input.length < 5));
emailD()
emailD()
can be used to verify that an unknown value is an email address string
.
Interface:
interface EmailDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function emailD(options?: EmailDecoderOptions): Decoder<string, any>;
Example:
// Decoder<string>;
const decoder = emailD();
exactlyD()
exactlyD()
accepts a value
argument and can be used to verify that an unknown input is === value
.
Interface:
interface ExactlyDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function exactlyD<T extends string | number | bigint | boolean>(
exact: T,
options?: ExactlyDecoderOptions,
): Decoder<T>;
function exactlyD<T>(exact: T, options?: ExactlyDecoderOptions): Decoder<T>;
Example:
// Decoder<'one'>;
const decoder = exactlyD('one');
instanceOfD()
instanceOfD()
accepts a javascript constructor argument and creates a decoder which verifies that its input is instanceof clazz
.
Interface:
interface InstanceOfDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function instanceOfD<T extends new (...args: any) => any>(
clazz: T,
options?: InstanceOfDecoderOptions,
): Decoder<InstanceType<T>, any>;
Example:
// Decoder<Map>;
const decoder = instanceOfD(Map);
integerD()
integerD()
can be used to verify that an unknown value is a whole number
.
Related:
Interface:
interface IntegerDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function integerD(options?: IntegerDecoderOptions): Decoder<number, any>;
Example:
// Decoder<number>;
const decoder = integerD();
lazyD()
lazyD()
recieves a function which returns a decoder and creates a new decoder which calls this function on each .decode()
call and uses the returned decoder to decode it's input. A common use case for this decoder is to decode recursive data structures. Alternate names for this decoder could be: recursive.
Interface:
interface LazyDecoderOptions {
decoderName?: string;
promise?: boolean;
}
export function lazyD<R, I = any>(
decoderFn: (value: I) => Decoder<R, I>,
options?: LazyDecoderOptions & { promise?: false },
): Decoder<R, I>;
export function lazyD<R, I = any>(
decoderFn: (
value: I,
) =>
| Decoder<R, I>
| AsyncDecoder<R, I>
| Promise<Decoder<R, I> | AsyncDecoder<R, I>>,
options: LazyDecoderOptions & { promise: true },
): AsyncDecoder<R, I>;
Example:
interface ArrayLike {
[key: number]: ArrayLike;
}
// Decoder<ArrayLike>
const decoder1 = arrayD(lazyD(() => decoder));
// Decoder<string | number>
const decoder2 = lazyD<string | number>(input =>
typeof input === 'number' ? integerD() : stringD(),
);
matchD()
matchD()
can be used to verify that an unknown value is a string
which conforms to the given RegExp
.
Interface:
interface MatchDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function matchD(
regex: RegExp,
options?: MatchDecoderOptions,
): Decoder<string, any>;
Example:
// Decoder<Date>
const decoder =
// this regex verifies that a string is of the form `YYYY-MM-DD`
matchD(/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/).map(
value => new Date(value),
);
neverD()
neverD()
creates a decoder which always returns DecoderError
with whatever input value is provided to it.
Interface:
interface NeverDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function neverD(options?: NeverDecoderOptions): Decoder<never, any>;
Example:
// Decoder<never>
const decoder = neverD();
nullableD()
nullableD()
accepts a decoder and returns a new decoder which accepts either the original decoder's value or null
.
Related:
Interface:
interface NullableDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function nullableD<R, I>(
decoder: Decoder<R, I>,
options?: NullableDecoderOptions,
): Decoder<R | null, I>;
function nullableD<R, I>(
decoder: AsyncDecoder<R, I>,
options?: NullableDecoderOptions,
): AsyncDecoder<R | null, I>;
Example:
// Decoder<string | null>
const decoder = nullableD(stringD());
numberD()
numberD()
can be used to verify that an unknown value is a number
.
Related:
Interface:
interface NumberDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function numberD(options?: NumberDecoderOptions): Decoder<number, any>;
Example:
// Decoder<number>
const decoder = numberD();
objectD()
"Object element" refers to a key: value
pair of the object. "Element-value" refers to the value
of this pair and "element-key" refers to the key
of this pair
objectD()
accepts a {[key: string]: Decoder<any> | AsyncDecoder<any>}
init object argument and returns a new decoder that will verify that an input is a non-null object, and that each element-key of the input is decoded by the corresponding element-key of the init object. On DecoderSuccess
, a new object is returned which has element-values defined by the init object's element-values. By default, any excess properties on the input object are ignored (i.e. not included on the returned value).
Options:
- If you pass the
noExcessProperties: true
option, any excess properties on the input object will return a DecoderError. - If you pass an
allErrors: true
option as well as any AsyncDecoders as arguments, thenobjectD()
will create a new AsyncDecoder which decodes each key of the input object in parallel. - If you pass the
removeUndefinedProperties: true
option, then after all other decoding of an input succeeds, anyundefined
properties are deleted from the result. - If you pass the
keepExcessProperties: true
option, then any excess properties on the input object are kept on the output object. This option is most useful when chaining multiple decoders together and when you only want to do a partial check on decodes after the first.
Related:
Interface:
interface ObjectDecoderOptions {
decoderName?: string;
allErrors?: boolean;
errorMsg?: DecoderErrorMsgArg;
data?: any;
noExcessProperties?: boolean;
removeUndefinedProperties?: boolean;
keepExcessProperties?: boolean;
}
function objectD<T>(
decoderObject: { [P in keyof T]: Decoder<T[P]> },
options?: ObjectDecoderOptions,
): Decoder<T>;
function objectD<T>(
decoderObject: { [P in keyof T]: Decoder<T[P]> | AsyncDecoder<T[P]> },
options?: ObjectDecoderOptions,
): AsyncDecoder<T>;
Example:
// Decoder<{one: string; two: number}>
const decoder1 = objectD({
one: stringD(),
two: numberD(),
});
// AsyncDecoder<{
// one: string;
// two: {
// three: number;
// four: string;
// };
// }>
const decoder2 = objectD({
one: stringD(),
two: objectD({
three: numberD(),
four: chainOf(
string(),
predicateD(input => Promise.resolve(true), {
promise: true,
}),
),
}),
});
optionalD()
optionalD()
accepts a decoder and returns a new decoder which accepts either the original decoder's value or null
or undefined
.
Related:
Interface:
interface OptionalDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function optionalD<R, I>(
decoder: Decoder<R, I>,
options?: OptionalDecoderOptions,
): Decoder<R | null | undefined, I>;
function optionalD<R, I>(
decoder: AsyncDecoder<R, I>,
options?: OptionalDecoderOptions,
): AsyncDecoder<R | null | undefined, I>;
Example:
// Decoder<string | null | undefined>
const decoder = optionalD(stringD());
predicateD()
predicateD()
accepts a predicate function argument and creates a decoder which verifies that inputs pass the function check.
Async: to pass a predicate function which returns a promise resolving to a boolean, pass the promise: true
option to predicateD()
.
Interface:
interface PredicateDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
promise?: boolean;
}
function predicateD<T>(
fn: (value: T) => boolean | Promise<boolean>,
options: PredicateDecoderOptions & { promise: true },
): AsyncDecoder<T, T>;
function predicateD<T>(
fn: (value: T) => boolean,
options?: PredicateDecoderOptions,
): Decoder<T, T>;
Example:
// Decoder<string, string>
const decoder = predicateD((input: string) => input.length > 5, {
errorMsg: 'must have length greater than 5',
});
stringD()
stringD()
can be used to verify that an unknown value is a string
.
Interface:
interface StringDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function stringD(options?: StringDecoderOptions): Decoder<string, any>;
Example:
// Decoder<string>
const decoder = stringD();
tupleD()
tupleD()
receives an array of decoders and creates a decoder which can be used to verify that an input is:
- An array of the same length as the decoder argument array.
- The first decoder argument will be used the process the first element of an input array.
- The second decoder argument will be used the process the second element of an input array.
- etc...
Options:
- If you pass an
allErrors: true
option as well as any AsyncDecoders as arguments, thentupleD()
will create a new AsyncDecoder which decodes each index of the input array in parallel.
Related:
Interface:
interface TupleDecoderOptions {
decoderName?: string;
allErrors?: boolean;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function tupleD<R extends [unknown, ...unknown[]]>(
decoders: { [P in keyof R]: Decoder<R[P]> },
options?: TupleDecoderOptions,
): Decoder<R>;
function tupleD<R extends [unknown, ...unknown[]]>(
decoders: { [P in keyof R]: Decoder<R[P]> | AsyncDecoder<R[P]> },
options?: TupleDecoderOptions,
): AsyncDecoder<R>;
Example:
// Decoder<[string, string]>
const decoder = tupleD([stringD(), uuidD()]);
undefinableD()
undefinableD()
accepts a decoder and returns a new decoder which accepts either the original decoder's value or undefined
.
Related:
Interface:
interface UndefinableDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function undefinableD<R, I>(
decoder: Decoder<R, I>,
options?: UndefinableDecoderOptions,
): Decoder<R | undefined, I>;
function undefinableD<R, I>(
decoder: AsyncDecoder<R, I>,
options?: UndefinableDecoderOptions,
): AsyncDecoder<R | undefined, I>;
Example:
// Decoder<string | undefined>
const decoder = undefinableD(stringD());
uuidD()
uuidD()
can be used to verify that an unknown value is a uuid v4 string
.
Interface:
interface UUIDDecoderOptions {
decoderName?: string;
errorMsg?: DecoderErrorMsgArg;
data?: any;
}
function uuidD(options?: UUIDDecoderOptions): Decoder<string, any>;
Example:
// Decoder<string>
const decoder = uuidD();
Similar projects
This repo has been inspired by a number of other decoder / validation libraries. If you are interested in functional programming, it is highly recommended you take a look at io-ts which has a functional design.