Utilities
Utility decoders for custom logic, preprocessing, and advanced use cases.
define
define(fn: (blob: unknown, ok, err) => DecodeResult<T>): Decoder<T> (source)
Defines a new Decoder<T>, by implementing a custom acceptance function. The function
receives three arguments:
blob- the raw/unknown input (aka your external data)ok- Callok(value)to accept the input and returnvalueerr- Callerr(message)to reject the input with errormessage
The expected return value should be a DecodeResult<T>, which can be obtained by
returning the result of calling the provided ok or err helper functions. Please note
that ok() and err() don't perform side effects! You'll need to return those values.
NOTE: This is the lowest-level API to define a new decoder, and therefore not recommended unless you have a very good reason for it. Most cases can be covered more elegantly by starting from an existing decoder and using
.transform()or.refine()on them instead.
// NOTE: Please do NOT implement an uppercase decoder like this! 😇
const uppercase: Decoder<string> = define((blob, ok, err) => {
if (typeof blob === 'string') {
// Accept the input
return ok(blob.toUpperCase());
} else {
// Reject the input
return err('I only accept strings as input');
}
});
// 👍
uppercase.verify('hi there') === 'HI THERE';
// 👎
uppercase.verify(123); // throws: 123
// ^^^ I only accept strings as inputThe above example is just an example to illustrate how define() works. It
would be more idiomatic to implement an uppercase decoder as follows:
const uppercase: Decoder<string> = string.transform((s) => s.toUpperCase());prep
prep(mapperFn: (raw: unknown) => unknown, decoder: Decoder<T>): Decoder<T> (source)
Pre-process the data input before passing it into the decoder. This gives you the ability
to arbitrarily customize the input on the fly before passing it to the decoder. Of course,
the input value at that point is still of unknown type, so you will have to deal with
that accordingly.
const decoder = prep(
// Will convert any input to an int first, before feeding it to
// positiveInteger. This will effectively also allow numeric strings
// to be accepted (and returned) as integers. If this ever throws,
// then the error message will be what gets annotated on the input.
(x) => parseInt(x),
positiveInteger,
);
// 👍
decoder.verify(42) === 42;
decoder.verify('3') === 3;
// 👎
decoder.verify('-3'); // throws: not a positive number
decoder.verify('hi'); // throws: not a numbernever
never: Decoder<never> (source)
fail: Decoder<never> (source)
Rejects all inputs, and always fails with the given error message. May be useful for explicitly disallowing keys, or for testing purposes.
const decoder = object({
a: string,
b: optional(never('Key b has been removed')),
});
// 👍
decoder.verify({ a: 'foo' }); // { a: 'foo' };
decoder.verify({ a: 'foo', c: 'bar' }); // { a: 'foo' };
// 👎
decoder.verify({ a: 'foo', b: 'bar' }); // throwsinstanceOf
instanceOf(klass: Klass<T>): Decoder<T> (source)
Accepts any value that is an instanceof the given class.
const decoder = instanceOf(Error);
// 👍
const value = new Error('foo');
decoder.verify(value) === value;
// 👎
decoder.verify('foo'); // throws
decoder.verify(3); // throwslazy
lazy(decoderFn: () => Decoder<T>): Decoder<T> (source)
Lazily evaluate the given decoder. This is useful to build self-referential types for recursive data structures.
type Tree = {
value: string;
children: Array<Tree>;
// ^^^^
// Self-reference defining a recursive type
};
const treeDecoder: Decoder<Tree> = object({
value: string,
children: array(lazy(() => treeDecoder)),
// ^^^^^^^^^^^^^^^^^^^^^^^
// Use lazy() like this to refer to the treeDecoder which is
// getting defined here
});