tiny-decoders
Advanced tools
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 @@ |
@@ -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" | ||
} | ||
} |
1017
README.md
@@ -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 | number | 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 | number | 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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
89067
827
1121