Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

tiny-decoders

Package Overview
Dependencies
Maintainers
1
Versions
30
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

tiny-decoders - npm Package Compare versions

Comparing version 2.0.0 to 3.0.0

189

CHANGELOG.md

@@ -0,1 +1,190 @@

### Version 3.0.0 (2019-08-08)
After using this library for a while in a real project, I found a bunch of
things that could be better. This version brings some bigger changes to the API,
making it more powerful and easier to use, and working better with TypeScript.
The new features adds half a kilobyte to the bundle, but it’s worth it.
- Added: When decoding arrays and objects, you can now opt into tolerant
decoding, where you can recover from errors, either by skipping values or
providing defaults. Whenever that happens, the message of the error that would
otherwise have been thrown is pushed to an `errors` array (`Array<string>`, if
provided), allowing you to inspect what was ignored.
- Added: A new `record` function. This makes renaming and combining fields much
easier, and allows decoding by type name easily without having to learn about
`andThen` and `fieldAndThen`. `field` has been integrated into `record` rather
than being its own decoder. The old `record` function is now called
`autoRecord`.
- Added: `tuple`. It’s like `record`, but for arrays/tuples.
- Added: `pair` and `triple`. These are convenience functions for decoding
tuples of length 2 and 3. I found myself decoding quite a few pairs and the
old way of doing it felt overly verbose. And the new `tuple` API wasn’t short
enough either for these common cases.
- Changed: `record` has been renamed to `autoRecord`. (A new function has been
added, and it’s called `record` but does not work like the old `record`.)
`autoRecord` also has a new TypeScript type annotation, which is better and
easier to understand.
- Changed: `fieldDeep` has been renamed to just `deep`, since `field` has been
removed.
- Removed: `group`. There’s no need for it with the new API. It was mostly used
to decode objects/records while renaming some keys. Many times the migration
is easy:
```ts
// Before:
group({
firstName: field("first_name", string),
lastName: field("last_name", string),
});
// After:
record(field => ({
firstName: field("first_name", string),
lastName: field("last_name", string),
}));
```
- Removed: `field`. It is now part of the new `record` and `tuple` functions
(for `tuple` it’s called `item`). If you used `field` to pluck a single value
you can migrate as follows:
```ts
// Before:
field("name", string);
field(0, string);
// After:
record(field => field("name", string));
tuple(item => item(0, string));
```
- Removed: `andThen`. I found no use cases for it after the new `record`
function was added.
- Removed: `fieldAndThen`. There’s no need for it with the new `record`
function. Here’s an example migration:
Before:
```ts
type Shape =
| {
type: "Circle";
radius: number;
}
| {
type: "Rectangle";
width: number;
height: number;
};
function getShapeDecoder(type: string): (value: unknown) => Shape {
switch (type) {
case "Circle":
return record({
type: () => "Circle",
radius: number,
});
case "Rectangle":
return record({
type: () => "Rectangle",
width: number,
height: number,
});
default:
throw new TypeError(`Invalid Shape type: ${repr(type)}`);
}
}
const shapeDecoder = fieldAndThen("type", string, getShapeDecoder);
```
After:
```ts
type Shape =
| {
type: "Circle";
radius: number;
}
| {
type: "Rectangle";
width: number;
height: number;
};
function getShapeDecoder(type: string): Decoder<Shape> {
switch (type) {
case "Circle":
return autoRecord({
type: () => "Circle",
radius: number,
});
case "Rectangle":
return autoRecord({
type: () => "Rectangle",
width: number,
height: number,
});
default:
throw new TypeError(`Invalid Shape type: ${repr(type)}`);
}
}
const shapeDecoder = record((field, fieldError, obj, errors) => {
const decoder = field("type", getShapeDecoder);
return decoder(obj, errors);
});
```
Alternatively:
```ts
type Shape =
| {
type: "Circle";
radius: number;
}
| {
type: "Rectangle";
width: number;
height: number;
};
const shapeDecoder = record(
(field, fieldError): Shape => {
const type = field("type", string);
switch (type) {
case "Circle":
return {
type: "Circle",
radius: field("radius", number),
};
case "Rectangle":
return autoRecord({
type: "Rectangle",
width: field("width", number),
height: field("height", number),
});
default:
throw fieldError("type", `Invalid Shape type: ${repr(type)}`);
}
}
);
```
### Version 2.0.0 (2019-06-07)

@@ -2,0 +191,0 @@

113

dist/index.d.ts

@@ -5,2 +5,4 @@ // TODO: TypeScript Version: 3.0

export type Decoder<T> = (value: unknown, errors?: Array<string>) => T;
export function boolean(value: unknown): boolean;

@@ -12,2 +14,6 @@

export function constant<
T extends boolean | number | string | undefined | null
>(constantValue: T): (value: unknown) => T;
export function mixedArray(value: unknown): ReadonlyArray<unknown>;

@@ -17,43 +23,59 @@

export function constant<
T extends boolean | number | string | undefined | null
>(constantValue: T): (value: unknown) => T;
export function array<T, U = T>(
decoder: Decoder<T>,
mode?: "throw" | "skip" | { default: U }
): Decoder<Array<T | U>>;
export function array<T>(
decoder: (value: unknown) => T
): (value: unknown) => Array<T>;
export function dict<T, U = T>(
decoder: Decoder<T>,
mode?: "throw" | "skip" | { default: U }
): Decoder<{ [key: string]: T | U }>;
export function dict<T>(
decoder: (value: unknown) => T
): (value: unknown) => { [key: string]: T };
export function record<T>(
callback: (
field: <U, V = U>(
key: string,
decoder: Decoder<U>,
mode?: "throw" | { default: V }
) => U | V,
fieldError: (key: string, message: string) => TypeError,
obj: { readonly [key: string]: unknown },
errors?: Array<string>
) => T
): Decoder<T>;
// Shamelessly stolen from:
// https://github.com/nvie/decoders/blob/1dc791f1df8e33110941baf5820f99318660f60f/src/object.d.ts#L4-L9
export type ExtractDecoderType<T> = T extends ((value: unknown) => infer V)
? V
: never;
export function tuple<T>(
callback: (
item: <U, V = U>(
index: number,
decoder: Decoder<U>,
mode?: "throw" | { default: V }
) => U | V,
itemError: (key: number, message: string) => TypeError,
arr: ReadonlyArray<unknown>,
errors?: Array<string>
) => T
): Decoder<T>;
export function group<T extends { [key: string]: (value: unknown) => unknown }>(
mapping: T
): (value: unknown) => { [key in keyof T]: ExtractDecoderType<T[key]> };
export function pair<T1, T2>(
decoder1: Decoder<T1>,
decoder2: Decoder<T2>
): Decoder<[T1, T2]>;
export function record<
T extends { [key: string]: (value: unknown) => unknown }
>(
mapping: T
): (value: unknown) => { [key in keyof T]: ExtractDecoderType<T[key]> };
export function triple<T1, T2, T3>(
decoder1: Decoder<T1>,
decoder2: Decoder<T2>,
decoder3: Decoder<T3>
): Decoder<[T1, T2, T3]>;
export function field<T>(
key: string | number,
decoder: (value: unknown) => T
): (value: unknown) => T;
export function autoRecord<T>(
mapping: { [key in keyof T]: Decoder<T[key]> }
): Decoder<T>;
export function fieldDeep<T>(
keys: Array<string | number>,
decoder: (value: unknown) => T
): (value: unknown) => T;
export function deep<T>(
path: Array<string | number>,
decoder: Decoder<T>
): Decoder<T>;
export function optional<T>(
decoder: (value: unknown) => T
): (value: unknown) => T | undefined;
export function optional<T>(decoder: Decoder<T>): Decoder<T | undefined>;
export function optional<T, U>(

@@ -65,23 +87,12 @@ decoder: (value: unknown) => T,

export function map<T, U>(
decoder: (value: unknown) => T,
fn: (value: T) => U
): (value: unknown) => U;
decoder: Decoder<T>,
fn: (value: T, errors?: Array<string>) => U
): Decoder<U>;
export function andThen<T, U>(
decoder: (value: unknown) => T,
fn: (value: T) => (value: unknown) => U
): (value: unknown) => U;
export function fieldAndThen<T, U>(
key: string | number,
decoder: (value: unknown) => T,
fn: (value: T) => (value: unknown) => U
): (value: unknown) => U;
export function either<T, U>(
decoder1: (value: unknown) => T,
decoder2: (value: unknown) => U
): (value: unknown) => T | U;
decoder1: Decoder<T>,
decoder2: Decoder<U>
): Decoder<T | U>;
export function lazy<T>(fn: () => (value: unknown) => T): (value: unknown) => T;
export function lazy<T>(callback: () => Decoder<T>): Decoder<T>;

@@ -88,0 +99,0 @@ export function repr(

@@ -7,15 +7,15 @@ "use strict";

exports.string = string;
exports.constant = constant;
exports.mixedArray = mixedArray;
exports.mixedDict = mixedDict;
exports.constant = constant;
exports.array = array;
exports.dict = dict;
exports.group = group;
exports.record = record;
exports.field = field;
exports.fieldDeep = fieldDeep;
exports.tuple = tuple;
exports.pair = pair;
exports.triple = triple;
exports.autoRecord = autoRecord;
exports.deep = deep;
exports.optional = optional;
exports.map = map;
exports.andThen = andThen;
exports.fieldAndThen = fieldAndThen;
exports.either = either;

@@ -50,2 +50,12 @@ exports.lazy = lazy;

function constant(constantValue) {
return function constantDecoder(value) {
if (value !== constantValue) {
throw new TypeError("Expected the value " + repr(constantValue) + ", but got: " + repr(value));
}
return constantValue;
};
}
function mixedArray(value) {

@@ -67,14 +77,8 @@ if (!Array.isArray(value)) {

function constant(constantValue) {
return function constantDecoder(value) {
if (value !== constantValue) {
throw new TypeError("Expected the value " + repr(constantValue) + ", but got: " + repr(value));
}
function array(decoder, mode) {
if (mode === void 0) {
mode = "throw";
}
return constantValue;
};
}
function array(decoder) {
return function arrayDecoder(value) {
return function arrayDecoder(value, errors) {
var arr = mixedArray(value); // Use a for-loop instead of `.map` to handle `array holes (`[1, , 2]`).

@@ -87,4 +91,28 @@ // A nicer way would be to use `Array.from(arr, (_, index) => ...)` but that

for (var index = 0; index < arr.length; index++) {
result.push(field(index, decoder)(arr));
for (var _index = 0; _index < arr.length; _index++) {
try {
var localErrors = [];
result.push(decoder(arr[_index], localErrors));
if (errors != null) {
for (var index2 = 0; index2 < localErrors.length; index2++) {
errors.push(keyErrorMessage(_index, arr, localErrors[index2]));
}
}
} catch (error) {
var _message = keyErrorMessage(_index, arr, error.message);
if (mode === "throw") {
error.message = _message;
throw error;
}
if (errors != null) {
errors.push(keyErrorMessage(_index, arr, error.message));
}
if (typeof mode !== "string") {
result.push(mode["default"]);
}
}
}

@@ -96,4 +124,8 @@

function dict(decoder) {
return function dictDecoder(value) {
function dict(decoder, mode) {
if (mode === void 0) {
mode = "throw";
}
return function dictDecoder(value, errors) {
var obj = mixedDict(value);

@@ -104,5 +136,30 @@ var keys = Object.keys(obj); // Using a for-loop rather than `.reduce` gives a nicer stack trace.

for (var index = 0; index < keys.length; index++) {
var key = keys[index];
result[key] = field(key, decoder)(obj);
for (var _index2 = 0; _index2 < keys.length; _index2++) {
var _key = keys[_index2];
try {
var localErrors = [];
result[_key] = decoder(obj[_key], localErrors);
if (errors != null) {
for (var index2 = 0; index2 < localErrors.length; index2++) {
errors.push(keyErrorMessage(_key, obj, localErrors[index2]));
}
}
} catch (error) {
var _message2 = keyErrorMessage(_key, obj, error.message);
if (mode === "throw") {
error.message = _message2;
throw error;
}
if (errors != null) {
errors.push(_message2);
}
if (typeof mode !== "string") {
result[_key] = mode["default"];
}
}
}

@@ -114,20 +171,106 @@

function group(mapping) {
return function groupDecoder(value) {
var keys = Object.keys(mapping); // Using a for-loop rather than `.reduce` gives a nicer stack trace.
function record(callback) {
return function recordDecoder(value, errors) {
var obj = mixedDict(value);
var result = {};
function field(key, decoder, mode) {
if (mode === void 0) {
mode = "throw";
}
for (var index = 0; index < keys.length; index++) {
var key = keys[index];
var decoder = mapping[key];
result[key] = decoder(value);
try {
var localErrors = [];
var result = decoder(obj[key], localErrors);
if (errors != null) {
for (var index2 = 0; index2 < localErrors.length; index2++) {
errors.push(keyErrorMessage(key, obj, localErrors[index2]));
}
}
return result;
} catch (error) {
var _message3 = keyErrorMessage(key, obj, error.message);
if (mode === "throw") {
error.message = _message3;
throw error;
}
if (errors != null) {
errors.push(_message3);
}
return mode["default"];
}
}
return result;
function fieldError(key, message) {
return new TypeError(keyErrorMessage(key, obj, message));
}
return callback(field, fieldError, obj, errors);
};
}
function record(mapping) {
return function recordDecoder(value) {
function tuple(callback) {
return function tupleDecoder(value, errors) {
var arr = mixedArray(value);
function item(index, decoder, mode) {
if (mode === void 0) {
mode = "throw";
}
try {
var localErrors = [];
var result = decoder(arr[index], localErrors);
if (errors != null) {
for (var index2 = 0; index2 < localErrors.length; index2++) {
errors.push(keyErrorMessage(index, arr, localErrors[index2]));
}
}
return result;
} catch (error) {
var _message4 = keyErrorMessage(index, arr, error.message);
if (mode === "throw") {
error.message = _message4;
throw error;
}
if (errors != null) {
errors.push(_message4);
}
return mode["default"];
}
}
function itemError(index, message) {
return new TypeError(keyErrorMessage(index, arr, message));
}
return callback(item, itemError, arr, errors);
};
}
function pair(decoder1, decoder2) {
// eslint-disable-next-line flowtype/require-parameter-type
return tuple(function pairDecoder(item) {
return [item(0, decoder1), item(1, decoder2)];
});
}
function triple(decoder1, decoder2, decoder3) {
// eslint-disable-next-line flowtype/require-parameter-type
return tuple(function tripleDecoder(item) {
return [item(0, decoder1), item(1, decoder2), item(2, decoder3)];
});
}
function autoRecord(mapping) {
return function autoRecordDecoder(value, errors) {
var obj = mixedDict(value);

@@ -138,10 +281,17 @@ var keys = Object.keys(mapping); // Using a for-loop rather than `.reduce` gives a nicer stack trace.

for (var index = 0; index < keys.length; index++) {
var key = keys[index];
var decoder = mapping[key];
for (var _index3 = 0; _index3 < keys.length; _index3++) {
var _key2 = keys[_index3];
var _decoder = mapping[_key2];
try {
result[key] = decoder(obj[key]);
var localErrors = [];
result[_key2] = _decoder(obj[_key2], localErrors);
if (errors != null) {
for (var index2 = 0; index2 < localErrors.length; index2++) {
errors.push(keyErrorMessage(_key2, obj, localErrors[index2]));
}
}
} catch (error) {
error.message = keyErrorMessage(key, obj, error.message);
error.message = keyErrorMessage(_key2, obj, error.message);
throw error;

@@ -155,33 +305,14 @@ }

function field(key, decoder) {
return function fieldDecoder(value) {
var obj = undefined;
var fieldValue = undefined;
if (typeof key === "string") {
obj = mixedDict(value);
fieldValue = obj[key];
} else {
obj = mixedArray(value);
fieldValue = obj[key];
}
try {
return decoder(fieldValue);
} catch (error) {
error.message = keyErrorMessage(key, obj, error.message);
throw error;
}
};
function deep(path, decoder) {
return path.reduceRight(function (nextDecoder, keyOrIndex) {
return typeof keyOrIndex === "string" ? // eslint-disable-next-line flowtype/require-parameter-type
record(function deepRecord(field) {
return field(keyOrIndex, nextDecoder);
}) : // eslint-disable-next-line flowtype/require-parameter-type
tuple(function deepTuple(item) {
return item(keyOrIndex, nextDecoder);
});
}, decoder);
}
function fieldDeep(keys, decoder) {
return function fieldDeepDecoder(value) {
var chainedDecoder = keys.reduceRight(function (childDecoder, key) {
return field(key, childDecoder);
}, decoder);
return chainedDecoder(value);
};
}
function optional(decoder, // This parameter is implicitly optional since `U` is allowed to be `void`

@@ -192,3 +323,3 @@ // (undefined), but don’ mark it with a question mark (`defaultValue?: U`)

defaultValue) {
return function optionalDecoder(value) {
return function optionalDecoder(value, errors) {
if (value == null) {

@@ -199,3 +330,3 @@ return defaultValue;

try {
return decoder(value);
return decoder(value, errors);
} catch (error) {

@@ -209,39 +340,16 @@ error.message = "(optional) " + error.message;

function map(decoder, fn) {
return function mapDecoder(value) {
return fn(decoder(value));
return function mapDecoder(value, errors) {
return fn(decoder(value, errors), errors);
};
}
function andThen(decoder, fn) {
return function andThenDecoder(value) {
// Run `value` through `decoder`, pass the result of that to `fn` and then
// run `value` through the return value of `fn`.
return fn(decoder(value))(value);
};
}
function fieldAndThen(key, decoder, fn) {
return function fieldAndThenDecoder(value) {
var keyValue = field(key, decoder)(value);
var finalDecoder = undefined;
try {
finalDecoder = fn(keyValue);
} catch (error) {
throw new TypeError(keyErrorMessage(key, value, error.message));
}
return finalDecoder(value);
};
}
var eitherPrefix = "Several decoders failed:\n";
function either(decoder1, decoder2) {
return function eitherDecoder(value) {
return function eitherDecoder(value, errors) {
try {
return decoder1(value);
return decoder1(value, errors);
} catch (error1) {
try {
return decoder2(value);
return decoder2(value, errors);
} catch (error2) {

@@ -259,5 +367,5 @@ error2.message = [eitherPrefix, stripPrefix(eitherPrefix, error1.message), "\n", stripPrefix(eitherPrefix, error2.message)].join("");

function lazy(fn) {
return function lazyDecoder(value) {
return fn()(value);
function lazy(callback) {
return function lazyDecoder(value, errors) {
return callback()(value, errors);
};

@@ -294,9 +402,9 @@ }

if (Array.isArray(value)) {
var arr = value;
var _arr = value;
if (!recurse && arr.length > 0) {
return toStringType + "(" + arr.length + ")";
if (!recurse && _arr.length > 0) {
return toStringType + "(" + _arr.length + ")";
}
var lastIndex = arr.length - 1;
var lastIndex = _arr.length - 1;
var items = []; // Print values around the provided key, if any.

@@ -311,7 +419,8 @@

for (var index = start; index <= end; index++) {
var item = index in arr ? repr(arr[index], {
for (var _index4 = start; _index4 <= end; _index4++) {
var _item = _index4 in _arr ? repr(_arr[_index4], {
recurse: false
}) : "<empty>";
items.push(index === key ? "(index " + index + ") " + item : item);
items.push(_index4 === key ? "(index " + _index4 + ") " + _item : _item);
}

@@ -327,6 +436,6 @@

if (toStringType === "Object") {
var obj = value;
var keys = Object.keys(obj); // `class Foo {}` has `toStringType === "Object"` and `rawName === "Foo"`.
var _obj = value;
var keys = Object.keys(_obj); // `class Foo {}` has `toStringType === "Object"` and `name === "Foo"`.
var name = obj.constructor.name;
var name = _obj.constructor.name;

@@ -344,3 +453,3 @@ if (!recurse && keys.length > 0) {

var _items = newKeys.slice(0, maxObjectChildren).map(function (key2) {
return printString(key2) + ": " + repr(obj[key2], {
return printString(key2) + ": " + repr(_obj[key2], {
recurse: false

@@ -369,3 +478,3 @@ });

// `maxLength` and `separator` could be taken as parameters and the offset
// could be calculated from them, but I've hardcoded them to save some bytes.
// could be calculated from them, but I’ve hardcoded them to save some bytes.
return str.length <= 20 ? str : [str.slice(0, 10), "…", str.slice(-9)].join("");

@@ -372,0 +481,0 @@ }

{
"name": "tiny-decoders",
"version": "2.0.0",
"version": "3.0.0",
"license": "MIT",

@@ -37,26 +37,26 @@ "author": "Simon Lydell",

"devDependencies": {
"@babel/cli": "7.4.4",
"@babel/core": "7.4.5",
"@babel/preset-env": "7.4.5",
"@babel/cli": "7.5.5",
"@babel/core": "7.5.5",
"@babel/preset-env": "7.5.5",
"@babel/preset-flow": "7.0.0",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "10.0.1",
"babel-eslint": "10.0.2",
"babel-jest": "24.8.0",
"decoders": "1.14.0",
"decoders": "1.15.0",
"doctoc": "1.4.0",
"dtslint": "0.7.8",
"eslint": "5.16.0",
"dtslint": "0.9.1",
"eslint": "6.1.0",
"eslint-config-lydell": "14.0.0",
"eslint-plugin-flowtype": "3.9.1",
"eslint-plugin-flowtype": "3.13.0",
"eslint-plugin-flowtype-errors": "4.1.0",
"eslint-plugin-import": "2.17.3",
"eslint-plugin-jest": "22.6.4",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jest": "22.15.0",
"eslint-plugin-prettier": "3.1.0",
"eslint-plugin-simple-import-sort": "3.1.1",
"flow-bin": "0.100.0",
"eslint-plugin-simple-import-sort": "4.0.0",
"flow-bin": "0.104.0",
"jest": "24.8.0",
"npm-run-all": "4.1.5",
"prettier": "1.18.0",
"prettier": "1.18.2",
"shelljs": "0.8.3"
}
}

@@ -6,3 +6,3 @@ # tiny-decoders [![Build Status][travis-badge]][travis-link] ![no dependencies][deps-tiny-decoders] [![minified size][min-tiny-decoders]][bundlephobia-tiny-decoders]

Supports [Flow] and [TypeScript].
Supports [TypeScript] and [Flow].

@@ -19,4 +19,6 @@ ## Contents

- [Intro](#intro)
- [A note on type annotations](#a-note-on-type-annotations)
- [API](#api)
- [Decoding primitive values](#decoding-primitive-values)
- [The `Decoder<T>` type](#the-decodert-type)
- [Primitive decoders](#primitive-decoders)
- [`boolean`](#boolean)

@@ -26,16 +28,14 @@ - [`number`](#number)

- [`constant`](#constant)
- [Decoding combined values](#decoding-combined-values)
- [Functions that _return_ a decoder](#functions-that-_return_-a-decoder)
- [Tolerant decoding](#tolerant-decoding)
- [`array`](#array)
- [`dict`](#dict)
- [`record`](#record)
- [`tuple`](#tuple)
- [`pair`](#pair)
- [`triple`](#triple)
- [`autoRecord`](#autorecord)
- [`deep`](#deep)
- [`optional`](#optional)
- [Decoding specific fields](#decoding-specific-fields)
- [`field`](#field)
- [`fieldDeep`](#fielddeep)
- [`group`](#group)
- [Chaining](#chaining)
- [`either`](#either)
- [`map`](#map)
- [`andThen`](#andthen)
- [`fieldAndThen`](#fieldandthen)
- [Less common decoders](#less-common-decoders)

@@ -64,4 +64,5 @@ - [`lazy`](#lazy)

```js
```ts
import {
Decoder,
array,

@@ -76,19 +77,21 @@ boolean,

type User = {|
name: string,
active: boolean,
age: ?number,
interests: Array<string>,
id: string | number,
|};
type User = {
name: string;
active: boolean;
age: number | undefined;
interests: Array<string>;
id: string | number;
};
const userDecoder: (mixed) => User = record({
name: string,
active: boolean,
age: optional(number),
interests: array(string),
id: either(string, number),
});
const userDecoder = record(
(field): User => ({
name: field("full_name", string),
active: field("is_active", boolean),
age: field("age", optional(number)),
interests: field("interests", array(string)),
id: field("id", either(string, number)),
})
);
const payload: mixed = getSomeJSON();
const payload: unknown = getSomeJSON();

@@ -112,21 +115,95 @@ const user: User = userDecoder(payload);

The central concept in tiny-decoders is the _decoder._ It’s a function that
turns `mixed` into some narrower type, or throws an error.
turns `unknown` (for Flow users: `mixed`) into some narrower type, or throws an
error.
For example, there’s a decoder called `string` (`(mixed) => string`) that
returns a string if the input is a string, and throws a `TypeError` otherwise.
That’s all there is to a decoder!
For example, there’s a decoder called `string` (`(value: unknown) => string`)
that returns a string if the input is a string, and throws a `TypeError`
otherwise. That’s all there is to a decoder!
tiny-decoders contains:
- A bunch of decoders (such as `(mixed) => string`).
- A bunch of functions that _return_ a decoder (such as `array`:
`((mixed) => T) => (mixed) => Array<T>`).
- [A bunch of decoders.][primitive-decoders]
- [A bunch of functions that _return_ a decoder.][returns-decoders]
Composing those functions together, you can _describe_ the shape of your objects
and let tiny-decoders verify that a given input matches that description.
and let tiny-decoders extract data that matches that description from a given
input.
Note that tiny-decoders is all about _extracting data,_ not validating that
input _exactly matches_ a schema.
## A note on type annotations
Most of the time, you don’t need to write any type annotations for decoders (but
some examples in the API documentation show them explicitly for clarity).
However, adding type annotations for record decoders results in much better
error messages. The following is the recommended way of annotating record
decoders, in both TypeScript and Flow:
```ts
import { record, autoRecord } from "tiny-decoders";
type Person = {
name: string;
age: number;
};
const personDecoder = record(
(field): Person => ({
name: field("name", string),
age: field("age", number),
})
);
const personDecoderAuto = autoRecord<Person>({
name: string,
age: number,
});
```
In TypeScript, you can also write it like this:
```ts
const personDecoder = record(field => ({
name: field("name", string),
age: field("age", number),
}));
const personDecoderAuto = autoRecord({
name: string,
age: number,
});
type Person = ReturnType<typeof personDecoder>;
// or:
type Person = ReturnType<typeof personDecoderAuto>;
```
See the [TypeScript type annotations example][typescript-type-annotations] and
the [Flow type annotations example][example-type-annotations] for more
information.
## API
### Decoding primitive values
### The `Decoder<T>` type
```ts
export type Decoder<T> = (value: unknown, errors?: Array<string>) => T;
```
This is a handy type alias for decoder functions.
Note that simple decoders that do not take an optional `errors` array are also
allowed by the above defintion:
```ts
(value: unknown) => T;
```
The type definition does not show that decoder functions throw `TypeError`s when
the input is invalid, but do keep that in mind.
### Primitive decoders
> Booleans, numbers and strings, plus [constant].

@@ -136,3 +213,5 @@

`(value: mixed) => boolean`
```ts
export function boolean(value: unknown): boolean;
```

@@ -143,3 +222,5 @@ Returns `value` if it is a boolean and throws a `TypeError` otherwise.

`(value: mixed) => number`
```ts
export function number(value: unknown): number;
```

@@ -150,3 +231,5 @@ Returns `value` if it is a number and throws a `TypeError` otherwise.

`(value: mixed) => string`
```ts
export function string(value: unknown): string;
```

@@ -157,18 +240,21 @@ Returns `value` if it is a string and throws a `TypeError` otherwise.

`(constantValue: T) => (value: mixed) => T`
```ts
export function constant<
T extends boolean | number | string | undefined | null
>(constantValue: T): (value: unknown) => T;
```
`T` must be one of `boolean | number | string | undefined | null`.
Returns a decoder. That decoder returns `value` if `value === constantValue` and
throws a `TypeError` otherwise.
Commonly used when [Decoding by type name][example-decoding-by-type-name].
Commonly used when [Decoding by type name][example-decoding-by-type-name] to
prevent mixups.
### Decoding combined values
### Functions that _return_ a decoder
> Arrays, objects and optional values.
> Decode arrays, objects and optional values. Combine decoders and functions.
For an array, you need to not just make sure that the value is an array, but
also that every item _inside_ the array has the correct type. Same thing for
objects (the values need to be checked). For this kind of cases you need to
objects (the values need to be checked). For these kinds of cases you need to
_combine_ decoders. This is done through functions that take a decoder as input

@@ -181,24 +267,65 @@ and returns a new decoder. For example, `array(string)` returns a decoder that

- If you know all the keys, use [record].
- If you know all the keys, use [record] or [autoRecord].
- If the keys are dynamic and all values have the same type, use [dict].
Related:
Some languages also have _tuples_ in addition to arrays. Both TypeScript and
Flow lets you use arrays as tuples if you want, which is also common in JSON.
Use [tuple], [pair] and [triple] to decode tuples.
- [Decoding tuples][example-tuples]
- The less common decoders [mixedArray] and [mixedDict].
(Related: The less common decoders [mixedArray] and [mixedDict].)
#### Tolerant decoding
Since arrays and objects can hold multiple values, their decoders allow opting
into tolerant decoding, where you can recover from errors, either by skipping
values or providing defaults. Whenever that happens, the message of the error
that would otherwise have been thrown is pushed to an `errors` array
(`Array<string>`, if provided), allowing you to inspect what was ignored.
(Perhaps not the most beautiful API, but very simple.)
For example, if you pass an `errors` array to a [record] decoder, it will both
push to the array and pass it along to its sub-decoders so they can push to it
as well.
#### `array`
`(decoder: (mixed) => T) => (value: mixed) => Array<T>`
```ts
export function array<T, U = T>(
decoder: Decoder<T>,
mode?: "throw" | "skip" | { default: U }
): Decoder<Array<T | U>>;
```
Takes a decoder as input, and returns a new decoder. The new decoder checks that
`value` is an array, and then runs the _input_ decoder on every item. If all of
that succeeds it returns `Array<T>`, otherwise it throws a `TypeError`.
its `unknown` input is an array, and then runs the _input_ decoder on every
item. What happens if decoding one of the items fails depends on the `mode`:
- `"throw"` (default): Throws a `TypeError` on the first invalid item.
- `"skip"`: Items that fail are ignored. This means that the decoded array can
be shorter than the input array – even empty! Error messages are pushed to the
`errors` array, if present.
- `{ default: U }`: The passed default value is used for items that fail. The
decoded array will always have the same length as the input array. Error
messages are pushed to the `errors` array, if present.
If no error was thrown, `Array<T>` is returned (or `Array<T | U>` if you use the
`{ default: U }` mode).
Example:
```js
```ts
import { array, string } from "tiny-decoders";
const arrayOfStringsDecoder: (mixed) => Array<string> = array(string);
const arrayOfStringsDecoder1: Decoder<Array<string>> = array(string);
const arrayOfStringsDecoder2: Decoder<Array<string>> = array(string, "skip");
const arrayOfStringsDecoder3: Decoder<Array<string>> = array(string, {
default: "",
});
// Decode an array of strings:
arrayOfStringsDecoder1(["a", "b", "c"]);
// Optionally collect error messages when `mode` isn’t `"throw"`:
const errors = [];
arrayOfStringsDecoder2(["a", 0, "c"], errors);
```

@@ -208,13 +335,43 @@

`(decoder: (mixed) => T) => (value: mixed) => { [string]: T }`
```ts
export function dict<T, U = T>(
decoder: Decoder<T>,
mode?: "throw" | "skip" | { default: U }
): Decoder<{ [key: string]: T | U }>;
```
Takes a decoder as input, and returns a new decoder. The new decoder checks that
`value` is an object, and then goes through all keys in the object and runs the
_input_ decoder on every value. If all of that succeeds it returns
`{ [string]: T }`, otherwise it throws a `TypeError`.
its `unknown` input is an object, and then goes through all keys in the object
and runs the _input_ decoder on every value. What happens if decoding one of the
key-values fails depends on the `mode`:
```js
- `"throw"` (default): Throws a `TypeError` on the first invalid item.
- `"skip"`: Items that fail are ignored. This means that the decoded object can
have fewer keys than the input object – it can even be empty! Error messages
are pushed to the `errors` array, if present.
- `{ default: U }`: The passed default value is used for items that fail. The
decoded object will always have the same set of keys as the input object.
Error messages are pushed to the `errors` array, if present.
If no error was thrown, `{ [key: string]: T }` is returned (or
`{ [key: string]: T | U }` if you use the `{ default: U }` mode).
```ts
import { dict, string } from "tiny-decoders";
const dictOfStringsDecoder: (mixed) => { [string]: T } = dict(string);
const dictOfStringsDecoder1: Decoder<{ [key: string]: string }> = dict(string);
const dictOfStringsDecoder2: Decoder<{ [key: string]: string }> = dict(
string,
"skip"
);
const dictOfStringsDecoder3: Decoder<{ [key: string]: string }> = dict(string, {
default: "",
});
// Decode an object of strings:
dictOfStringsDecoder1({ a: "1", b: "2" });
// Optionally collect error messages when `mode` isn’t `"throw"`:
const errors = [];
dictOfStringsDecoder2({ a: "1", b: 0 }, errors);
```

@@ -224,186 +381,309 @@

`(mapping: Mapping) => (value: mixed) => Result`
```ts
export function record<T>(
callback: (
field: <U, V = U>(
key: string,
decoder: Decoder<U>,
mode?: "throw" | { default: V }
) => U | V,
fieldError: (key: string, message: string) => TypeError,
obj: { readonly [key: string]: unknown },
errors?: Array<string>
) => T
): Decoder<T>;
```
- `Mapping`:
Takes a callback function as input, and returns a new decoder. The new decoder
checks that its `unknown` input is an object, and then calls the callback (the
object is passed as the `obj` parameter). The callback receives a `field`
function that is used to pluck values out of object. The callback is allowed to
return anything, and that is the `T` of the decoder.
```
{
key1: (mixed) => A,
key2: (mixed) => B,
...
keyN: (mixed) => C,
}
```
`field("key", decoder)` essentially runs `decoder(obj["key"])` but with better
error messages and automatic handling of the `errors` array, if provided. The
nice thing about `field` is that it does _not_ return a new decoder – but the
value of that field! This means that you can do for instance
`const type: string = field("type", string)` and then use `type` however you
want inside your callback.
- `Result`:
`fieldError("key", "message")` creates an error message for a certain key.
`throw fieldError("key", "message")` gives an error that lets you know that
something is wrong with `"key"`, while `throw new TypeError("message")` would
not be as clear. Useful when [Decoding by type
name][example-decoding-by-type-name].
```
{
key1: A,
key2: B,
...
keyN: C,
}
```
`obj` and `errors` are passed in case you’d need them for some edge case, such
as if you need to [distinguish between undefined, null and missing
values][example-missing-values].
Takes a “Mapping” as input, and returns a decoder. The new decoder checks that
`value` is an object, and then goes through all the key-decoder pairs in the
_Mapping._ For every key, the value of `value[key]` must match the key’s
decoder. If all of that succeeds it returns “Result,” otherwise it throws a
`TypeError`. The Result is identical to the Mapping, except all of the
`(mixed) =>` are gone, so to speak.
Note that if your input object and the decoded object look exactly the same and
you don’t need any advanced features it’s often more convenient to use
[autoRecord].
Example:
```ts
import {
Decoder,
record,
boolean,
number,
string,
optional,
repr,
} from "tiny-decoders";
```js
import { record, string, number, boolean } from "tiny-decoders";
type User = {
age: number;
active: boolean;
name: string;
description: string | undefined;
legacyId: string | undefined;
version: 1;
};
type User = {|
name: string,
age: number,
active: boolean,
|};
const userDecoder = record(
(field): User => ({
// Simple field:
age: field("age", number),
// Renaming a field:
active: field("is_active", boolean),
// Combining two fields:
name: `${field("first_name", string)} ${field("last_name", string)}`,
// Optional field:
description: field("description", optional(string)),
// Allowing a field to fail:
legacyId: field("extra_data", number, { default: undefined }),
// Hardcoded field:
version: 1,
})
);
const userDecoder: (mixed) => User = record({
name: string,
age: number,
active: boolean,
});
```
const userData: unknown = {
age: 30,
is_active: true,
first_name: "John",
last_name: "Doe",
};
Notes:
// Decode a user:
userDecoder(userData);
- `record` is a convenience function around [group] and [field]. Check those out
if you need more flexibility, such as renaming fields!
// Optionally collect error messages from fields where `mode` isn’t `"throw"`:
const errors = [];
userDecoder(userData, errors);
- The `value` we’re decoding is allowed to have extra keys not mentioned in the
`record` mapping. I haven’t found a use case where it is useful to disallow
extra keys. This package is about extracting data in a type-safe way, not
validation.
type Shape =
| {
type: "Circle";
radius: number;
}
| {
type: "Rectangle";
width: number;
height: number;
};
- Want to _add_ some extra keys? Checkout the [extra
fields][example-extra-fields] example.
// Decoding by type name:
const shapeDecoder = record(
(field): Shape => {
const type = field("type", string);
switch (type) {
case "Circle":
return {
type: "Circle",
radius: field("radius", number),
};
- There’s a way to let Flow infer types from your record decoders (or any
decoder actually) if you want to take the DRY principle to the extreme – see
the [inference example][example-inference].
case "Rectangle":
return {
type: "Rectangle",
width: field("width", number),
height: field("height", number),
};
- The _actual_ type annotation for this function is a bit weird but does its job
(with good error messages!) – check out the source code if you’re interested.
default:
throw fieldError("type", `Invalid Shape type: ${repr(type)}`);
}
}
);
#### `optional`
// Plucking a single field out of an object:
const ageDecoder: Decoder<number> = record(field => field("age", number));
```
`(decoder: (mixed) => T, defaultValue?: U) => (value: mixed) => Array<T | U>`
#### `tuple`
Takes a decoder as input, and returns a new decoder. The new decoder returns
`defaultValue` if `value` is undefined or null, and runs the _input_ decoder on
`value` otherwise. (If you don’t supply `defaultValue`, undefined is used as the
default.)
```ts
export function tuple<T>(
callback: (
item: <U, V = U>(
index: number,
decoder: Decoder<U>,
mode?: "throw" | { default: V }
) => U | V,
itemError: (key: number, message: string) => TypeError,
arr: ReadonlyArray<unknown>,
errors?: Array<string>
) => T
): Decoder<T>;
```
This is especially useful to mark fields as optional in a [record]:
Takes a callback function as input, and returns a new decoder. The new decoder
checks that its `unknown` input is an array, and then calls the callback.
`tuple` is just like `record`, but for tuples (arrays) instead of for records
(objects). Instead of a `field` function, there’s an `item` function that let’s
you pluck out items of the tuple/array.
```js
import { optional, record, string, number, boolean } from "tiny-decoders";
Note that you can return any type from the callback, not just tuples. If you’d
rather have a record you could return that.
type User = {|
name: string,
age: ?number,
active: boolean,
|};
```ts
import { Decoder, tuple, number, string } from "tiny-decoders";
const userDecoder: (mixed) => User = record({
name: string,
age: optional(number),
active: optional(boolean, true),
});
type Person = {
firstName: string;
lastName: string;
age: number;
description: string;
};
// Decoding a tuple into a record:
const personDecoder = tuple(
(item): Person => ({
firstName: item(0, string),
lastName: item(1, string),
age: item(2, number),
description: item(3, string),
})
);
// Taking the first number from an array:
const firstNumberDecoder: Decoder<number> = tuple(item => item(0, number));
```
In the above example:
See also [Decoding tuples][example-tuples].
- `.name` must be a string.
- `.age` is allowed to be undefined, null or missing (defaults to `undefined`).
- `.active` defaults to `true` if it is undefined, null or missing.
Most tuples are 2 or 3 in length. If you want to decode such a tuple into a
TypeScript/Flow tuple it’s usually more convenient to use [pair] and [triple].
If the need should ever arise, check out the example on how to [distinguish
between undefined, null and missing values][example-missing-values].
tiny-decoders treats undefined, null and missing values the same by default, to
keep things simple.
#### `pair`
### Decoding specific fields
```ts
export function pair<T1, T2>(
decoder1: Decoder<T1>,
decoder2: Decoder<T2>
): Decoder<[T1, T2]>;
```
> Parts of objects and arrays, plus [group].
A convenience function around [tuple] when you want to decode `[x, y]` into
`[T1, T2]`.
#### `field`
```ts
import { Decoder, pair, number } from "tiny-decoders";
`(key: string | number, decoder: (mixed) => T) => (value: mixed) => T`
const pointDecoder: Decoder<[number, number]> = pair(number, number);
```
Takes a key (object key, or array index) and a decoder as input, and returns a
new decoder. The new decoder checks that `value` is an object (if key is a
string) or an array (if key is a number), and runs the _input_ decoder on
`value[key]`. If both of those checks succeed it returns `T`, otherwise it
throws a `TypeError`.
See also [Decoding tuples][example-tuples].
This lets you pick a single value out of an object or array.
#### `triple`
`field` is typically used with [group].
```ts
export function triple<T1, T2, T3>(
decoder1: Decoder<T1>,
decoder2: Decoder<T2>,
decoder3: Decoder<T3>
): Decoder<[T1, T2, T3]>;
```
Examples:
A convenience function around [tuple] when you want to decode `[x, y, z]` into
`[T1, T2, T3]`.
```js
import { field, group, string, number } from "tiny-decoders";
```ts
import { Decoder, triple, number } from "tiny-decoders";
type Person = {|
firstName: string,
lastName: string,
|};
const coordinateDecoder: Decoder<[number, number, number]> = pair(
number,
number,
number
);
```
// You can use `field` with `group` to rename keys on a record.
const personDecoder: (mixed) => Person = group({
firstName: field("first_name", string),
lastName: field("last_name", string),
});
See also [Decoding tuples][example-tuples].
type Point = {|
x: number,
y: number,
|};
#### `autoRecord`
// If you want to pick out items at certain indexes of an array, treating it
// is a tuple, use `field` and save the results in a `group`.
// This will decode `[4, 7]` into `{ x: 4, y: 7 }`.
const pointDecoder: (mixed) => Point = group({
x: field(0, number),
y: field(1, number),
```ts
export function autoRecord<T>(
mapping: { [key in keyof T]: Decoder<T[key]> }
): Decoder<T>;
```
Suppose you have a record `T`. Now make an object that looks just like `T`, but
where every value is a decoder for its key. `autoRecord` takes such an object –
called `mapping` – as input and returns a new decoder. The new decoder checks
that its `unknown` input is an object, and then goes through all the key-decoder
pairs in the `mapping`. For every key, `mapping[key](value[key])` is run. If all
of that succeeds it returns a `T`, otherwise it throws a `TypeError`.
Example:
```ts
import { autoRecord, boolean, number, string } from "tiny-decoders";
type User = {
name: string;
age: number;
active: boolean;
};
const userDecoder = autoRecord<User>({
name: string,
age: number,
active: boolean,
});
```
Full examples:
Notes:
- [Decoding tuples][example-tuples]
- [Renaming fields][example-renaming-fields]
- [Decoding by type name][example-decoding-by-type-name]
- `autoRecord` is a convenience function instead of [record]. Check out [record]
if you need more flexibility, such as renaming fields!
#### `fieldDeep`
- The `unknown` input value we’re decoding is allowed to have extra keys not
mentioned in the `mapping`. I haven’t found a use case where it is useful to
disallow extra keys. This package is about extracting data in a type-safe way,
not validation.
`(keys: Array<string | number>, decoder: (mixed) => T) => (value: mixed) => T`
- Want to _add_ some extra keys? Checkout the [extra
fields][example-extra-fields] example.
#### `deep`
```ts
export function deep<T>(
path: Array<string | number>,
decoder: Decoder<T>
): Decoder<T>;
```
Takes an array of keys (object keys, and array indexes) and a decoder as input,
and returns a new decoder. It works like `field`, but repeatedly goes deeper and
deeper using the given `keys`. If all of those checks succeed it returns `T`,
otherwise it throws a `TypeError`.
and returns a new decoder. It repeatedly goes deeper and deeper into its
`unknown` input using the given `path`. If all of those checks succeed it
returns `T`, otherwise it throws a `TypeError`.
`fieldDeep` is used to pick a one-off value from a deep structure, rather than
having to decode each level manually with [record] and [array].
`deep` is used to pick a one-off value from a deep structure, rather than having
to decode each level manually with [record] and [tuple]. See the [Deep
example][example-deep].
Note that `fieldDeep([], decoder)` is equivalent to just `decoder`.
Note that `deep([], decoder)` is equivalent to just `decoder`.
You probably want to [combine `fieldDeep` with `either`][example-allow-failures]
since reaching deeply into structures is likely to fail.
You might want to [combine `deep` with `either`][example-deep] since reaching
deeply into structures is likely to fail.
Examples:
```js
import { fieldDeep, number, either } from "tiny-decoders";
```ts
import { deep, number, either } from "tiny-decoders";
const accessoryPriceDecoder: (mixed) => number = fieldDeep(
const accessoryPriceDecoder: Decoder<number> = deep(
["store", "products", 0, "accessories", 0, "price"],

@@ -413,3 +693,3 @@ number

const accessoryPriceDecoderWithDefault: (mixed) => number = either(
const accessoryPriceDecoderWithDefault: Decoder<number> = either(
accessoryPriceDecoder,

@@ -420,135 +700,74 @@ () => 0

#### `group`
#### `optional`
`(mapping: Mapping) => (value: mixed) => Result`
```ts
export function optional<T>(decoder: Decoder<T>): Decoder<T | undefined>;
export function optional<T, U>(
decoder: (value: unknown) => T,
defaultValue: U
): (value: unknown) => T | U;
```
- `Mapping`:
Takes a decoder as input, and returns a new decoder. The new decoder returns
`defaultValue` if its `unknown` input is undefined or null, and runs the _input_
decoder on the `unknown` otherwise. (If you don’t supply `defaultValue`,
undefined is used as the default.)
```
{
key1: (mixed) => A,
key2: (mixed) => B,
...
keyN: (mixed) => C,
}
```
This is especially useful to mark fields as optional in a [record] or
[autoRecord]:
- `Result`:
```ts
import { autoRecord, optional, boolean, number, string } from "tiny-decoders";
```
{
key1: A,
key2: B,
...
keyN: C,
}
```
type User = {
name: string;
age: number | undefined;
active: boolean;
};
Takes a “Mapping” as input, and returns a decoder. The new decoder goes through
all the key-decoder pairs in the _Mapping._ For every key-decoder pair, `value`
must match the decoder. (The keys don’t matter – all their decoders are run on
the same `value`). If all of that succeeds it returns “Result,” otherwise it
throws a `TypeError`. The Result is identical to the Mapping, except all of the
`(mixed) =>` are gone, so to speak.
As you might have noticed, `group` has the exact same type annotation as
[record]. So what’s the difference? [record] is all about decoding objects with
certain keys. `group` is all about running several decoders on _the same value_
and saving the results. If all of the decoders in the Mapping succeed, an object
with named values is returned. Otherwise, a `TypeError` is thrown.
If you’re familiar with [Elm’s mapping functions][elm-map], `group` plus [map]
replaces all of those. For example, Elm’s `map3` function lets you run three
decoders. You are then given the result values in the same order, allowing you
to do something with them. With `group` you combine _any_ number of decoders,
and it lets you refer to the result values by name instead of order (reducing
the risk of mix-ups).
`group` is typically used with [field] to decode objects where you want to
rename the fields.
Example:
```js
import { group, field, string, number, boolean } from "tiny-decoders";
const userDecoder = group({
firstName: field("first_name", string),
lastName: field("last_name", string),
age: field("age", number),
active: field("active", boolean),
const userDecoder = autoRecord<User>({
name: string,
age: optional(number),
active: optional(boolean, true),
});
```
It’s also possible to [rename only some fields][example-renaming-fields] without
repetition if you’d like.
In the above example:
The _actual_ type annotation for this function is a bit weird but does its job
(with good error messages!) – check out the source code if you’re interested.
- `.name` must be a string.
- `.age` is allowed to be undefined, null or missing (defaults to `undefined`).
- `.active` defaults to `true` if it is undefined, null or missing.
### Chaining
If the need should ever arise, check out the example on how to [distinguish
between undefined, null and missing values][example-missing-values].
tiny-decoders treats undefined, null and missing values the same by default, to
keep things simple.
> Two decoders chained together in different ways, plus [map].
#### `map`
#### `either`
`(decoder1: (mixed) => T, decoder2: (mixed) => U) => (value: mixed) => T | U`
Takes two decoders as input, and returns a new decoder. The new decoder tries to
run the _first_ input decoder on `value`. If that succeeds, it returns `T`,
otherwise it tries the _second_ input decoder. If _that_ succeeds it returns
`U`, otherwise it throws a `TypeError`.
Example:
```js
import { either, string, number } from "tiny-decoders";
const stringOrNumberDecoder: (mixed) => string | number = either(
string,
number
);
```ts
export function map<T, U>(
decoder: Decoder<T>,
fn: (value: T, errors?: Array<string>) => U
): Decoder<U>;
```
What if you want to try _three_ (or more) decoders? You’ll need to nest another
`either`:
```js
import { either, string, number, boolean } from "tiny-decoders";
const weirdDecoder: (mixed) => string | number | boolean = either(
string,
either(number, boolean)
);
```
That’s perhaps not very pretty, but not very common either. It’s possible to
make `either2`, `either3`, etc functions, but I don’t think it’s worth it.
You can also use `either` to [allow decoders to fail][example-allow-failures]
and to [distinguish between undefined, null and missing
values][example-missing-values].
#### `map`
`(decoder: (mixed) => T, fn: (T) => U): (value: mixed) => U`
Takes a decoder and a function as input, and returns a new decoder. The new
decoder runs the _input_ decoder on `value`, and then passes the result to the
provided function. That function can return a transformed result. It can also be
another decoder. If all of that succeeds it returns `U` (the return value of
`fn`), otherwise it throws a `TypeError`.
decoder runs the _input_ decoder on its `unknown` input, and then passes the
result to the provided function. That function can return a transformed result.
It can also be another decoder. If all of that succeeds it returns `U` (the
return value of `fn`), otherwise it throws a `TypeError`.
Example:
```js
import { map, array, number } from "tiny-decoders";
```ts
import { Decoder, map, array, number } from "tiny-decoders";
const numberSetDecoder: (mixed) => Set<number> = map(
const numberSetDecoder: Decoder<Set<number>> = map(
array(number),
(arr) => new Set(arr)
arr => new Set(arr)
);
const nameDecoder: (mixed) => string = map(
record({
const nameDecoder: Decoder<string> = map(
autoRecord({
firstName: string,

@@ -559,2 +778,7 @@ lastName: string,

);
// But the above is actually easier with `record`:
const nameDecoder2: Decoder<string> = record(
field => `${field("firstName", string)} ${field("lastName", string)}`
);
```

@@ -565,59 +789,65 @@

- [Decoding Sets][example-sets]
- [Decoding tuples][example-custom-decoders]
- [Decoding tuples][example-tuples]
- [Adding extra fields][example-extra-fields]
- [Renaming fields][example-custom-decoders]
- [Renaming fields][example-renaming-fields]
- [Custom decoders][example-custom-decoders]
#### `andThen`
#### `either`
`(decoder: (mixed) => T, fn: (T) => (mixed) => U): (value: mixed) => U`
```ts
export function either<T, U>(
decoder1: Decoder<T>,
decoder2: Decoder<U>
): Decoder<T | U>;
```
Takes a decoder and a function as input, and returns a new decoder. The new
decoder runs the _input_ decoder on `value`, and then passes the result to the
provided function. That function must return yet another decoder. That final
decoder is then run on the same `value` as before. If all of that succeeds it
returns `U`, otherwise it throws a `TypeError`.
Takes two decoders as input, and returns a new decoder. The new decoder tries to
run the _first_ input decoder on its `unknown` input. If that succeeds, it
returns `T`, otherwise it tries the _second_ input decoder. If _that_ succeeds
it returns `U`, otherwise it throws a `TypeError`.
This is used when you need to decode a value a little bit, _and then_ decode it
some more based on the first decoding result.
Example:
The most common case is to first decode a “type” field of an object, and then
choose a decoder based on that. Since that is so common, there’s actually a
special decoder for that – [fieldAndThen] – with a better error message.
```ts
import { Decoder, either, number, string } from "tiny-decoders";
So when do you need `andThen`? Here are some examples:
const stringOrNumberDecoder: Decoder<string | number> = either(string, number);
```
- When `fieldAndThen` isn’t enough: The second example in [Decoding by type
name][example-decoding-by-type-name].
- If you ever have to [distinguish between undefined, null and missing
values][example-missing-values].
What if you want to try _three_ (or more) decoders? You’ll need to nest another
`either`:
#### `fieldAndThen`
```ts
import { Decoder, either, boolean, number, string } from "tiny-decoders";
`(key: string | number, decoder: (mixed) => T, fn: (T) => (mixed) => U) => (value: mixed) => U`
const weirdDecoder: Decoder<string | number | boolean> = either(
string,
either(number, boolean)
);
```
`fieldAndThen(key, decoder, fn)` is equivalent to
`andThen(field(key, decoder), fn)` but has a better error message. In other
words, it takes the combined parameters of [field] and [andThen] and returns a
new decoder.
That’s perhaps not very pretty, but not very common either. It would of course
be possible to add functions like `either2`, `either3`, etc, but I don’t think
it’s worth it.
See [Decoding by type name][example-decoding-by-type-name] for an example and
comparison with `andThen(field(key, decoder), fn)`.
You can also use `either` [distinguish between undefined, null and missing
values][example-missing-values].
### Less common decoders
> Recursive structures, and less precise objects and arrays.
> Recursive structures, as well as less precise objects and arrays.
Related:
Related: [Decoding `unknown` values.][example-mixed]
- [Decoding `mixed` values][example-mixed]
#### `lazy`
`(fn: () => (mixed) => T) => (value: mixed) => T`
```ts
export function lazy<T>(callback: () => Decoder<T>): Decoder<T>;
```
Takes a function that returns a decoder as input, and returns a new decoder. The
new decoder runs the function to get the _input_ decoder, and then runs the
input decoder on `value`. If that succeeds it returns `T` (the return value of
the input decoder), otherwise it throws a `TypeError`.
Takes a callback function that returns a decoder as input, and returns a new
decoder. The new decoder runs the callback function to get the _input_ decoder,
and then runs the input decoder on its `unknown` input. If that succeeds it
returns `T` (the return value of the input decoder), otherwise it throws a
`TypeError`.

@@ -631,48 +861,19 @@ `lazy` lets you decode recursive structures. `lazy(() => decoder)` is equivalent

Examples:
Since [record] and [tuple] take callbacks themselves, lazy is not needed most of
the time. But `lazy` can come in handy for [array] and [dict].
```js
import { lazy, record, array, string } from "tiny-decoders";
See the [Recursive example][example-recursive] to learn when and how to use this
decoder.
// A recursive data structure:
type Person = {|
name: string,
friends: Array<Person>,
|};
#### `mixedArray`
// Attempt one:
const personDecoder: (mixed) => Person = record({
name: string,
friends: array(personDecoder), // ReferenceError: personDecoder is not defined
});
// Attempt two:
const personDecoder: (mixed) => Person = record({
name: string,
friends: lazy(() => array(personDecoder)), // No errors!
});
```ts
export function mixedArray(value: unknown): ReadonlyArray<unknown>;
```
[Full recursive example][example-recursive]
If you use the [no-use-before-define] ESLint rule, you need to disable it for
the `lazy` line:
```js
const personDecoder: (mixed) => Person = record({
name: string,
// eslint-disable-next-line no-use-before-define
friends: lazy(() => array(personDecoder)),
});
```
#### `mixedArray`
`(value: mixed) => $ReadOnlyArray<mixed>`
Usually you want to use [array] instead. `array` actually uses this decoder
behind the scenes, to verify that `value` is an array (before proceeding to
decode every item of the array). `mixedArray` _only_ checks that `value` is an
array, but does not care about what’s _inside_ the array – all those values stay
as `mixed`.
behind the scenes, to verify that its `unknown` input is an array (before
proceeding to decode every item of the array). `mixedArray` _only_ checks that
its `unknown` input is an array, but does not care about what’s _inside_ the
array – all those values stay as `unknown`.

@@ -684,9 +885,12 @@ This can be useful for custom decoders, such as when [distinguishing between

`(value: mixed) => { +[string]: mixed }`
```ts
export function mixedDict(value: unknown): { readonly [key: string]: unknown };
```
Usually you want to use [dict] or [record] instead. `dict` and `record` actually
use this decoder behind the scenes, to verify that `value` is an object (before
proceeding to decode values of the object). `mixedDict` _only_ checks that
`value` is an object, but does not care about what’s _inside_ the object – all
the keys remain unknown and their values stay as `mixed`.
use this decoder behind the scenes, to verify that its `unknown` input is an
object (before proceeding to decode values of the object). `mixedDict` _only_
checks that its `unknown` input is an object, but does not care about what’s
_inside_ the object – all the keys remain unknown and their values stay as
`unknown`.

@@ -698,3 +902,13 @@ This can be useful for custom decoders, such as when [distinguishing between

`(value: mixed, options?: Options) => string`
```ts
export function repr(
value: unknown,
options?: {
key?: string | number;
recurse?: boolean;
maxArrayChildren?: number;
maxObjectChildren?: number;
}
): string;
```

@@ -706,12 +920,12 @@ Takes any value, and returns a string representation of it for use in error

| name | type | default | description |
| ----------------- | --------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------- |
| key | <code>string &vert; number &vert; void</code> | `undefined` | An object key or array index to highlight when `repr`ing objects or arrays. |
| recurse | `boolean` | `true` | Whether to recursively call `repr` on array items and object values. It only recurses once. |
| maxArrayChildren | `number` | `5` | The number of array items to print (when `recurse` is `true`.) |
| maxObjectChildren | `number` | `3` | The number of object key-values to print (when `recurse` is `true`.) |
| name | type | default | description |
| ----------------- | -------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------- |
| key | <code>string &vert; number &vert; undefined</code> | `undefined` | An object key or array index to highlight when `repr`ing objects or arrays. |
| recurse | `boolean` | `true` | Whether to recursively call `repr` on array items and object values. It only recurses once. |
| maxArrayChildren | `number` | `5` | The number of array items to print (when `recurse` is `true`.) |
| maxObjectChildren | `number` | `3` | The number of object key-values to print (when `recurse` is `true`.) |
Example:
```js
```ts
import { repr } from "tiny-decoders";

@@ -750,4 +964,4 @@

- [nvie/decoders]: Larger API, fancier error messages, larger size.
- tiny-decoders: Smaller (slightly different) API, kinda good error messages,
smaller size.
- tiny-decoders: Smaller (and slightly different) API, kinda good error
messages, smaller size.

@@ -844,3 +1058,3 @@ ### Error messages

You need [Node.js] 10 and npm 6.
You need [Node.js] 12 and npm 6.

@@ -878,4 +1092,4 @@ ### npm scripts

<!-- prettier-ignore-start -->
[andThen]: #andThen
[array]: #array
[autoRecord]: #autoRecord
[babel]: https://babeljs.io/

@@ -896,4 +1110,4 @@ [bundlephobia-decoders]: https://bundlephobia.com/result?p=decoders

[example-decoding-by-type-name]: https://github.com/lydell/tiny-decoders/blob/master/examples/decoding-by-type-name.test.js
[example-deep]: https://github.com/lydell/tiny-decoders/blob/master/examples/deep.test.js
[example-extra-fields]: https://github.com/lydell/tiny-decoders/blob/master/examples/extra-fields.test.js
[example-inference]: https://github.com/lydell/tiny-decoders/blob/master/examples/inference.test.js
[example-missing-values]: https://github.com/lydell/tiny-decoders/blob/master/examples/missing-values.test.js

@@ -906,6 +1120,4 @@ [example-mixed]: https://github.com/lydell/tiny-decoders/blob/master/examples/mixed.test.js

[example-tuples]: https://github.com/lydell/tiny-decoders/blob/master/examples/tuples.test.js
[field]: #field
[fieldandthen]: #fieldandthen
[example-type-annotations]: https://github.com/lydell/tiny-decoders/blob/master/examples/type-annotations.test.js
[flow]: https://flow.org/
[group]: #group
[jest]: https://jestjs.io/

@@ -919,12 +1131,17 @@ [map]: #map

[mixeddict]: #mixeddict
[no-use-before-define]: https://eslint.org/docs/rules/no-use-before-define
[node.js]: https://nodejs.org/en/
[npm]: https://www.npmjs.com/
[nvie/decoders]: https://github.com/nvie/decoders
[pair]: #pair
[prettier]: https://prettier.io/
[primitive-decoders]: #primitive-decoders
[record]: #record
[result]: https://github.com/nvie/lemons.js#result
[returns-decoders]: #functions-that-return-a-decoder
[travis-badge]: https://travis-ci.com/lydell/tiny-decoders.svg?branch=master
[travis-link]: https://travis-ci.com/lydell/tiny-decoders
[triple]: #triple
[tuple]: #tuple
[typescript-type-annotations]: https://github.com/lydell/tiny-decoders/blob/master/typescript/type-annotations.ts
[typescript]: https://www.typescriptlang.org/
<!-- prettier-ignore-end -->

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc