Turn arbitrary JavaScript values into valid TypeScript values. Inspired by Elm's JSON Decoder library.
This library is great at the edges of your program when you want to validate the structure of data not created by your program (for example: HTTP responses, user input, functions that return any
).
To find an example of this library used for an HTTP response, check the example
directory. It is OK to to transform the input into a data structure that makes more sense for your application. For example, the data returned by the CDC in the example
directory is an array with the values in different indexes. You can transform that array of values into a structured object. It also returns number as strings, which you can change as well.
import Decoder, { Decoded } from 'decoder-js';
const idDecoder: Decoder<number> = Decoder.number();
const successfulResult: Decoded<number> = idDecoder.run(1);
// successfulResult = success(1)
const failedResult: Decoded<number> = idDecoder.run('1');
// failedResult = failure(nonEmptyArray.of('Value must be a number, found "string" instead'))
Note: This library uses Giulio Canti's functional library to provide different data structures like Option
and NonEmptyArray
. Let me know if you need any help with this library.
A value that knows how to turn unknown inputs into typed outputs.
Succeeds with a boolean value.
import Decoder from 'decoder-js';
Decoder.boolean().run(true);
// Success: true
Decoder.boolean().run(false);
// Success: false
Decoder.boolean().run(10);
// Failure
Decoder.boolean().run('string');
// Failure
Succeeds with a number value.
import Decoder from 'decoder-js';
Decoder.number().run(10);
// Success: 10
Decoder.number().run(false);
// Failure
Decoder.number().run('string');
// Failure
Succeeds with a string value.
import Decoder from 'decoder-js';
Decoder.string().run('string');
// Success: 'string'
Decoder.string().run(false);
// Failure
Decoder.string().run(10);
// Failure
Succeeds with an array of values.
import Decoder from 'decoder-js';
Decoder.array(Decoder.boolean()).run([true, false, true]);
// Success: [true, false, true]
Decoder.array(Decoder.number()).run([10, 11, 12]);
// Success: [10, 11, 12]
Succeeds with a non-empty array of values. This decoders guarantees at least one value in the array.
import Decoder from 'decoder-js';
Decoder.nonEmptyArray(Decoder.boolean()).run([true, false, true]);
// Success: NonEmptyArray(true, [false, true])
Decoder.nonEmptyArray(Decoder.boolean()).run([]);
// Failure
Succeeds with null or a value.
import Decoder from 'decoder-js';
Decoder.nullable(Decoder.boolean()).run(null);
// Success: None
Decoder.nullable(Decoder.boolean()).run(true);
// Success: Some(true)
Decoder.nullable(Decoder.boolean()).run('string');
// Failure
Succeeds when an object contains a certain field.
import Decoder from 'decoder-js';
Decoder.field('isAdult', Decoder.boolean()).run({ isAdult: true });
// Success: true
Decoder.field('isAdult', Decoder.boolean()).run({ isAdult: true, licenseState: 'California' });
// Success: true
Decoder.field('isAdult', Decoder.boolean()).run({ isAdult: 'true' });
// Failure
Decoder.field('isAdult', Decoder.boolean()).run({ licenseState: 'Vermont' });
// Failure
The object can have other fields. Lots of them! The only thing this decoder cares about is if isAdult
is present and that the value there is a boolean.
Check out map2
to see how to decode multiple fields!
Similar to field
, but succeeds whether the value is present or not.
import Decoder from 'decoder-js';
Decoder.optionalField('isAdult', Decoder.boolean()).run({ isAdult: true });
// Success: Some(true)
Decoder.optionalField('isAdult', Decoder.boolean()).run({ isAdult: true, licenseState: 'California' });
// Success: Some(true)
Decoder.optionalField('isAdult', Decoder.boolean()).run({ isAdult: 'true' });
// Failure
Decoder.optionalField('isAdult', Decoder.boolean()).run({ licenseState: 'Vermont' });
// Success: None
Succeeds when an object contains nested fields.
import Decoder from 'decoder-js';
const person = { name: 'Jane Doe', info: { isAdult: true, height: 161.8 } };
Decoder.at(['info', 'isAdult'], Decoder.boolean()).run(person);
// Success: true
Decoder.at(['info', 'height'], Decoder.number()).run(person);
// Success: 161.8
This is really just a shorthand for saying things like:
Decoder.field('info', Decoder.field('isAdult', Decoder.boolean())).run(person);
Succeeds when an array contains a certain index.
import Decoder from 'decoder-js';
const names = ['Alice', 'Bob', 'Chuck'];
Decoder.index(0, Decoder.string()).run(names);
// Success: 'Alice'
Decoder.index(1, Decoder.string()).run(names);
// Success: 'Bob'
Decoder.index(2, Decoder.string()).run(names);
// Success: 'Chuck'
Decoder.index(3, Decoder.string()).run(names);
// Failure
Try a bunch of different decoders. This can be useful if the input may come in a couple different formats. For example, say you want to read an array of numbers, but some of them are null.
import Decoder from 'decoder-js';
import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray';
const badNumbers = [1, 2, null, 4];
const badNumberDecoder = Decoder.oneOf(
new NonEmptyArray(Decoder.number(), [Decoder.null(0)])
);
Decoder.list(badNumberDecoder).run(badNumbers);
// Success: [1, 2, 0, 4]
Why would someone generate data like this? Questions like this are not good for your health. The point is that you can use oneOf
to handle situations like this!
You could also use oneOf
to help version your data. Try the latest format, then a few older ones that you still support. You could use andThen
to be even more particular if you wanted.
Transform a decoder. Maybe you just want to know an unsigned number:
import Decoder from 'decoder-js';
const unsignedNumberDecoder: Decoder<number> = Decoder.map(Math.abs, Decoder.number());
unsignedNumberDecoder.run(-1);
// Success: 1
It is often helpful to use map with oneOf, like when defining nullable
:
import Decoder from 'decoder-js';
import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray';
function nullable<a>(decoder: Decoder<a>): Decoder<Option<a>> {
const decoders = new NonEmptyArray(
Decoder.null(OptionFn.none),
[Decoder.map<a, Option<a>>(OptionFn.some, decoder)],
);
return Decoder.oneOf(decoders);
}
Try two decoders and then combine the result. We can use this to decode objects with many fields:
import Decoder from 'decoder-js';
interface IPoint {
x: number;
y: number;
}
function point(x: number, y: number): IPoint {
return { x, y };
}
const pointDecoder = Decoder.map2(
point,
Decoder.field('x', Decoder.number()),
Decoder.field('y', Decoder.number()),
);
pointDecoder.run({ x: 2, y: 4 });
// Success: IPoint { x: 2, y: 4 }
It tries each individual decoder and puts the result together with the point
function.
Try three decoders and then combine the result. We can use this to decode objects with many fields:
import Decoder from 'decoder-js';
interface IPerson {
name: string;
height: number;
isAdult: boolean;
}
function person(name: string, height: number, isAdult: boolean): IPerson {
return { name, height, isAdult };
}
const personDecoder = Decoder.map3(
person,
Decoder.field('name', Decoder.string()),
Decoder.at(['info', 'height'], Decoder.number()),
Decoder.at(['info', 'isAdult'], Decoder.boolean()),
);
personDecoder.run({ name: 'Tester', info: { height: 1.8, isAdult: true } });
// Success: IPerson { name: 'Tester', height: 1.8, isAdult: true }
Like map2
, it tries each decoder and then give the results to the person
function. That can be any function though!
When you want to try more than three decoders, you can use andMap
to partially decode a large function of arbitrary length.
import { Curried3, curry, pipe } from 'fp-ts/lib/function';
import Decoder from 'decoder-js';
interface IUser {
id: number;
username: string;
email: string;
isAdmin: boolean;
}
function user(id: number, username: string, email: string, isAdmin: boolean): IUser {
return { id, username, email, isAdmin };
}
type FirstDecoder = Curried3<string, string, boolean, IUser>;
const userDecoder = pipe(
Decoder.andMap<number, FirstDecoder>(Decoder.field('id', Decoder.number())),
Decoder.andMap(Decoder.field('username', Decoder.string())),
Decoder.andMap(Decoder.field('email', Decoder.string())),
Decoder.andMap(Decoder.field('isAdmin', Decoder.boolean())),
)(Decoder.succeed(curry(user)));
userDecoder.run({ id: 0, username: 'username', email: 'email', isAdmin: true });
// Success: IUser { id: 0, username: 'username', email: 'email', isAdmin: true }
Note: andMap
is meant to be used with succeed
and fp-ts
's curry
and pipe
functions.
Do not do anything with the input. This can be useful if you have particularly complex data that you would like to deal with later, or if you are going to send it out and do not care about its structure.
Decode a null
value into some other value.
import Decoder from 'decoder-js';
Decoder.null(false).run(null);
// Success: false
Decoder.null(42).run(null);
// Success: 42
Decoder.null(42).run(42);
// Failure
Decoder.null(42).run(false);
// Failure
So if you ever see a null
, this will return whatever value you specified.
Ignore the input and produce a certain output.
import Decoder from 'decoder-js';
Decoder.succeed(42).run(true);
// Success: 42
Decoder.succeed(42).run([1, 2, 3]);
// Success: 42
Decoder.succeed(42).run('hello');
// Success: 42
Decoder.succeed(42).run({ x: 1, y: 2 });
// Success: 42
This is handy when used with oneOf
or andThen
.
Ignore the input and make the decoder fail. This is handy when used with oneOf
or andThen
where you want to give a custom error message in some case.
See the andThen
docs for an example.
Create decoders that depend on previous results. If you are creating a Direction decoder, you might do something like this:
import Decoder from 'decoder-js';
enum Direction {
Up,
Down,
Left,
Right,
}
function directionDecoder(type: string): Decoder<Direction> {
switch (type) {
case 'Up':
return Decoder.succeed(Direction.Up);
case 'Down':
return Decoder.succeed(Direction.Down);
case 'Left':
return Decoder.succeed(Direction.Left);
case 'Right':
return Decoder.succeed(Direction.Right);
default:
return Decoder.fail(`Value "${type}" is not a valid Direction: "Up", "Down", "Left", "Right"`);
}
}
Decoder.andThen(directionDecoder, Decoder.string()).run('Up');
// Succeed: Direction.Up