elmish-decoder
TypeScript icon, indicating that this package has built-in type declarations

2.0.2 • Public • Published

elmish-decoder

Turn (untyped) any values (for example coming from a configuration file or some external API) into type-checked and type-safe Typescript values, without having to remember to update your schema/validation code manually! Instead, elmish-decoders tries to use Typescript's type system as best as it can to catch most mistakes at compile-time, given sufficient type annotations.

This library is heavily inspired by Elm's Json.Decode, but adds some Typescript-specific features to it, making it easier to use while also catching more mistakes at compile time (compared to other Typescript libraries of course).

If you are comfortable with Elm/ML-like syntax, I highly recommend checking out their Intro to JSON decoders for an introduction behind the concepts of this library.

Getting started

Setup

elmish-decoder was tested using the following versions:

  • Node ^8.10
  • Typescript ^3.1.0

This library works best if you use Typescript's strict mode, enabling it to catch unhandled optional fields, unsafe Decoder casts, and incompatible constructor signatures.

To install the package, simply use npm. Types are already included!

$ npm install --save elmish-decoder

A simple package.json Decoder

Also try this out for yourself, and see what happens if you change the types!

import Decoder from 'elmish-decoder';
 
import fs = require('fs');
import semver = require('semver');
 
// A simplified version of the full package.json - Type
 
interface Dependencies {
    [packageName: string]: semver.Range
}
 
interface PackageJson {
    private?: boolean;
    name: string;
    version: semver.SemVer,
    dependencies: Dependencies
    devDependencies: Dependencies
}
 
// Parse a string, then pass it to the parseRange function
const rangeDecoder = Decoder.class(semver.Range, Decoder.string, Decoder.succeedOpt(true));
 
// Parse a string, then pass it to the parseVersion function.
// semver.parse returns `null` if the version is not valid, so
// we use `Decoder.filter` to validate the version afterwards.
const semverDecoder =
    Decoder.function(semver.parse, Decoder.string, Decoder.succeedOpt(true))
        .filterOptional("Version is not valid!");
 
// Dependencies always contain a Range as their value.
// There are no required fields, so the second parameter be empty.
const dependenciesDecoder = Decoder.dict<Dependencies>(rangeDecoder, {});
 
// Here we combine everything together to a PackageJson - Interface Decoder.
const packageJsonDecoder = Decoder.obj<PackageJson>({
    private: Decoder.optional(Decoder.boolean),
    name: Decoder.string,
    version: semverDecoder,
    dependencies: dependenciesDecoder,
    devDependencies: dependenciesDecoder
});
 
 
const packageJsonStr = fs.readFileSync('./package.json', 'utf-8');
// Try it out on the local package.json!
// `runString` calls the first (left) function on a failure, and the second (right) function on success.
packageJsonDecoder.runString(console.error, console.log, packageJsonStr);

API Documentation

Index

This is an alphabetical list of all methods. If you know what you need and just want to see the documentation for it, you can use those links. Otherwise, I recommend reading the documentation in order.

Properties:

Static Functions:

Instance Methods:

<Class> Decoder<T>

This class itself represents a Decoder knowing how to turn an any into a T.

Running Decoders

decodeString

decodeString(jsonString: string): T

Parses the given string into a JSON value and run the decoder on it. This function will fail if the input string is not valid JSON, or if the Decoder fails for some reason. throws: {DecoderError} An exception containing a list of all the things that went wrong.

Parameters:

Param Type
jsonString string

Returns: T The decoded object.


decodeValue

decodeValue(input: any): T

Decodes a given value, returning a type-checked, fully constructed version of it. This function will fail with an exception if the Decoder fails for some reason. throws: {DecoderError} An exception containing a list of all the things that went wrong.

Parameters:

Param Type
input any

Returns: T The decoded object.


runString

runString<U>(Left: function, Right: function, jsonString: string): U

Parse a given string into a JSON value and run the decoder on it. Instead of throwing an exception, this method uses the functional approach of returning a 'Either' instance to distinquish success from failure.

This function catches any JSON errors and packages them into a Left. Any other exceptions will not be caught by using this method.

Parameters:

Param Type Description
Left function 'Either' object constructor for the Failure case
Right function 'Either' object constructor for the Success case
jsonString string The string to parse and decode

Returns: U


runValue

runValue<U>(Left: function, Right: function, input: any): U

Decode a value, putting the result into an Either type. Instead of throwing an exception, this method uses the functional approach of returning a 'Either' instance to distinquish success from failure.

This function will not catch exceptions for you, but allows you to handle Decoder errors in a more functional way.

Internally, this library uses a custom 'Result' Applicative type.

Parameters:

Param Type Description
Left function 'Either' object constructor for the Failure case
Right function 'Either' object constructor for the Success case
input any

Returns: U


Primitives

<Static> any

● any: Decoder<any>

Do not decode the value, but return it as an untyped any. This can be useful if you have a complex data structure you want to deal with later, or you just need to extract this value to pass it on, but you don't care about its structure.


<Static> boolean

● boolean: Decoder<boolean>

Decode a boolean. Only real boolean objects will be accepted, so a string property "true" or a numeric value 1 will result in a failure.

Decoder.boolean.decodeValue(true)   // true
Decoder.boolean.decodeValue(1)      // DecoderError
Decoder.boolean.decodeValue("true") // DecoderError

<Static> float

● float: Decoder<number>

Similar to Decoder.number, this decoder accepts all floating point numbers, but does not allow for special values like NaN or Infinite, even if the object itself would be a valid number object.

Decoder.float.decodeValue(42)          // ==> 42
Decoder.float.decodeValue("42")        // ==> DecoderError
Decoder.float.decodeValue("NaN")       // ==> DecoderError
Decoder.float.decodeValue(Infinity)    // ==> DecoderError
Decoder.float.decodeValue("-Infinity") // ==> DecoderError

<Static> int

● int: Decoder<number>

Decode a number that is an integer. In contrast to the Decoder.number decoder, this does not accept NaN or Infinite values, and also rejects all numbers that cannot be encoded as a 32-bit integer value, including all floating-point numbers.

Decoder.int.decodeValue(42)              // ==> 42
Decoder.int.decodeValue(-0)              // ==> -0
Decoder.int.decodeValue(0.5)             // ==> DecoderError
Decoder.int.decodeValue(Math.pow(2, 32)) // ==> DecoderError

<Static> number

● number: Decoder<number>

Decode a number. Only real number objects will be accepted. Strings are generally not seen as numbers, even if they only contain digits. The only exceptions to this rule are special string for "NaN" and "Infinity", respectively.

Decoder.number.decodeValue(42)          // ==> 42
Decoder.number.decodeValue("42")        // ==> DecoderError
Decoder.number.decodeValue("NaN")       // ==> NaN
Decoder.number.decodeValue(Infinity)    // ==> Infinity
Decoder.number.decodeValue("-Infinity") // ==> -Infinity

<Static> string

● string: Decoder<string>

Decode string values. Only actual strings will be accepted by this decoder, so values like null or any other value that could be .toString() - ed are going to be rejected.

If you want to accept optional strings, consider using the optional modifier, or create Decoder chains using Decoder.oneOf.

Decoder.string.decodeValue("")                            // ==> ""
Decoder.string.decodeValue("Hello, Sailor!")              // ==> "Hello, Sailor!"
Decoder.string.decodeValue(null)                          // ==> DecoderError
Decoder.string.decodeValue({ toString() { return ""; } }) // ==> DecoderError

Composite Types

<Static> obj

obj<T>(configuration: DecoderTable<T>): Decoder<T>

Create a typesafe decoder for an interface or a type alias. The configuration object you pass in has the same shape as the type to decode, but you have to specify decoders for all the members instead. This ensures that your compile-time type definition and your run-time decoder definition will always stay in sync with one another.

The shape of the configuration object is the same as the shape of your interface. If your JSON has a slightly different shape, you can pass a Decoder.field or an Decoder.fieldOpt as one of the parameters instead, ignoring the default mapping.

If your JSON structure and your target interface structure are vastly different, you might want to consider creating intermediate DTO interfaces instead, and using Decoder.map to build your final objects.

Example:

interface Point {
    x: number;
    y: number;
}
 
// ok: configuration has the same shape and the same types as Point
const pointDecoder = Decoder.obj<Point>({
    x: Decoder.float,
    y: Decoder.float
})
 
// ok: configuration has the same shape as the Point interface, but x/y got renamed to u/v
const uvDecoder = Decoder.obj<Point>({
    x: Decoder.field('u', Decoder.float),
    y: Decoder.field('v', Decoder.float)
})
 
// compile error: configuration is missing a field
const pointDecoder2 = Decoder.obj<Point>({
    x: Decoder.float
})
 
// compile error: configuration and interface types don't match
const pointDecoder3 = Decoder.obj<Point>({
    x: Decoder.integer,
    y: Decoder.integer
})
 
// compile error: configuration declares a non-optional field as optional
const pointDecoder4 = Decoder.obj<Point>({
    x: Decoder.integer,
    y: Decoder.integer.optional()
})

Parameters:

Param Type
configuration DecoderTable<T>

Returns: Decoder<T>


<Static> dict

dict<TDict,TValue>(valueDecoder: Decoder<TValue>, explicitConfiguration: DecoderTable<Explicit<TDict>>): Decoder<TDict>

Create a decoder for types with an index signature. Because index signatures can optionally have required keys, this function not only takes a default valueDecoder, but also an explicitConfiguration map in the same format as the one for Decoder.obj, allowing for explicit control over those fields.

Example:

type ComplexKey = { key: string, ctrl?: boolean, alt?: boolean, shift?: boolean };
type Key = string | ComplexKey;
 
type Keymap = {
    [command: string]: Key,
    // editing keymap needs a keybinding, to prevent the user from locking themselfes out
    "settings.editKeymap": Key
};
 
const keyDecoder =
    Decoder.string.orElse(
    Decoder.obj<ComplexKey>({
        key: Decoder.string,
        ctrl: Decoder.optional(Decoder.boolean),
        alt: Decoder.optional(Decoder.boolean),
        shift: Decoder.optional(Decoder.boolean)
    }));
 
const keymapDecoder = Decoder.dict<Keymap>(keyDecoder, {
    "settings.editKeymap": keyDecoder
});

Parameters:

Param Type
valueDecoder Decoder<TValue>
explicitConfiguration DecoderTable<Explicit<TDict>>

Returns: Decoder<TDict>


<Static> tuple

tuple<Args>(...decoders: DecoderTuple<Args>): Decoder<Args>

Decode a tuple of known parameter types.

const keyValueDecoder = Decoder.tuple<[string, string]>(Decoder.string, Decoder.string);
 
keyValueDecoder.decodeValue(["msg", "Hello, Sailor!"]); // OK
keyValueDecoder.decodeValue(["msg"]) // Err - second tuple argument missing
keyValueDecoder.decodeValue({0: "msg", 1: "Hello, Sailor!"}) // Err - not an array

While Decoder.optional works for allowing null - values inside the tuple, it will NOT accept non-existent tuple elements. Similar to optional function paramters, this could lead to confusing results once optional and required values are mixed.

Instead, I recommend using Decoder.oneOf or Decoder.andThen to dispatch to multiple different decoders for each number of values.

Parameters:

Param Type
Rest decoders DecoderTuple<Args>

Returns: Decoder<Args>


<Static> array

array<T>(itemDecoder: Decoder<T>): Decoder<T[]>

Convert a regular decoder into a decoder for arrays of that type. Array decoders only succeed if all items can be successfully decoded. If this is not your desired behaviour, you can use Decoder.orDefaultValue and Decoder.map to return default values and (optionally) filter those out instead.

Example:

const d1 = Decoder.array(Decoder.int);
 
d1.decodeValue([3, 4, 5])   // ==> [3, 4, 5]
d1.decodeValue([3, "4", 5]) // ==> DecoderError ...
d1.decodeValue([])          // ==> []
 
d1.decodeValue({0: 3, 1: 4, 2: 5, length: 3}) // ==> DecoderError ...

Parameters:

Param Type
itemDecoder Decoder<T>

Returns: Decoder<T[]>


<Static> class

class<T,Args>(constructor: object, ...argsDecoders: DecoderTuple<Args>): Decoder<T>

Create a decoder that extracts values to pass to a class constructor.

Example:

class Point {
    constructor(
        public readonly x: number,
        public readonly y: number
    ) { }
}
 
const pointDecoder = Decoder.class(Point,
    Decoder.field('x', Decoder.int),
    Decoder.field('y', Decoder.int));

Parameters:

Param Type Description
constructor object A class constructor to use to instanciate the resulting object.
Rest argsDecoders DecoderTuple<Args> A decoder for each parameter of the constructor.

Returns: Decoder<T>


<Static> function

function<T,Args>(constructor: function, ...argsDecoders: DecoderTuple<Args>): Decoder<T>

Create a decoder that extracts values to pass to a value-constructing function. This is very similar to a static version of Decoder.map that supports multiple arguments.

Parameters:

Param Type Description
constructor function A function to call to instanciate the resulting object.
Rest argsDecoders DecoderTuple<Args> A decoder for each parameter of the function.

Returns: Decoder<T>


Advanced objects and tuples

<Static> tag

tag<T>(value: T): Decoder<T>

Creates a Decoder that checks for some hardcoded primitive value, as used in literal types or discriminated unions. In contrast to the succeed decoder, this one does not always return the same value, but only accepts excatly one value as the only valid one.

Parameters:

Param Type
value T

Returns: Decoder<T>


<Static> optional

optional<T>(decoder: Decoder<T>, defaultValue?: T): OptionalDecoder<T>

Makes a decoder that doesn't fail on null or undefined, but instead returns null, or a custom value.

You may notice that this function returns an OptionalDecoder<T> instead of your regular Decoder<T>. This is a hack used to enable typechecks on optional fields, both classes are practially and semantically identical.

Also note that this library works best with strictNullChecks enabled. If you don't use this option, every value can potentially be null, so every decoder has to handle null values at some point, which makes everything safe, but is not very pleasent to work with.

Example:

// This assumes strictNullChecks
interface Point {
    x: number,
    y: number,
    z?: number
}
 
// Error TS2345: Argument of type { ... } is not assignable to parameter of type 'DecoderTable<Point>'.
//   Types of property 'z' are incompatible.
//     Type 'Decoder<number>' is not assignable to type 'OptionalDecoder<number|undefined>'.
//       Property '__you_tried_to_use_a_non_optional_Decoder_on_an_optional_field__' is missing in type 'Decoder<number>'.
const pointDecoder = Decoder.obj<Point>({
    x: Decoder.float,
    y: Decoder.float,
    z: Decoder.float
});
 
// OK - Decoder.optional returns the right type
const pointDecoder = Decoder.obj<Point>({
    x: Decoder.float,
    y: Decoder.float,
    z: Decoder.optional(Decoder.float)
});

Parameters:

Param Type
decoder Decoder<T>
Optional defaultValue T

Returns: OptionalDecoder<T>


<Static> lazy

lazy<T>(decoder: function): Decoder<T>

Lazy makes working with recursive data structures, like nested comments, less painfull.

In Typescript, you cannot use a const or let value before it is assigned. Wrapping the decoder inside a Decoder.lazy(() => decoder) will work around that, but Typescript will still complain to you that now the type of decoder cannot be inferred, because it is recursively used in its own definition, so we also need an additional type annotation.

Please also make sure to never wrap Decoder.field or Decoder.fieldOpt directly inside of lazy. Because of laziness, the Decoder doesn't know yet wether or not it decodes a field or not, so dispatching to the field decoder will not work correctly. Instead, you can wrap the fieldDecoder inside.

Example:

interface Comment {
    message: string;
    responses: Comment[]
}
 
const commentDecoder = Decoder.obj<Comment>({
    message: Decoder.string,
    // Important: Wrap the Decoder.array instead of the Decoder.field for the remapping of the properties to work!
    responses: Decoder.field('replies', Decoder.lazy(() => Decoder.array(commentDecoder)))
});

Parameters:

Param Type
decoder function

Returns: Decoder<T>


<Static> lazyOpt

lazyOpt<T>(decoder: function): OptionalDecoder<T>

Parameters:

Param Type
decoder function

Returns: OptionalDecoder<T>


<Static> field

field<T>(field: string, fieldDecoder: Decoder<T>): Decoder<T>

Decodes a single field of an object using a fieldDecoder, or fail if the value is not an object, or the field doesn't exist.

Note that this is meaningfully different of a field that has a value of null, which the fieldDecoder might want to handle seperately.

This function usually get's called automatically by Decoder.obj, but you might find it usefull for decoding a single field type, or for changing the structure in an object decoder.

Parameters:

Param Type
field string
fieldDecoder Decoder<T>

Returns: Decoder<T>


<Static> fieldOpt

fieldOpt<T>(field: string, fieldDecoder: Decoder< T | null>, defaultValue?: T): OptionalDecoder<T>

Decode a single optional field of an object using the fieldDecoder. In contrast to Decoder.field, this decoder will not immediately fail if the field does not exist, but instead relay a null or defaultValue to the underlying fieldDecoder, if that decoder is setup to work on optional values.

To ensure decoders can handle optional values, it is usually best to wrap them inside of Decoder.optional last.

Remember that a field being optional is different from a value being optional! This function deals with the former case, which are those cases where the field is actually missing. The value of a field that is not missing can still be null though, which is handled by Decoder.optional.

Example:

const optionalRevisionDecoder = Decoder.fieldOpt('rev', Decoder.optional(Decoder.int), '')

Parameters:

Param Type
field string
fieldDecoder Decoder< T | null>
Optional defaultValue T

Returns: OptionalDecoder<T>


<Static> index

index<T>(index: number, indexDecoder: Decoder<T>): Decoder<T>

Decode a single entry of an array using the indexDecoder. This is similar to Decoder.field, but for arrays. Since this function checks to see if the input is actually a real array, please use that function instead if your object just happens to have a numeric key.

This function usually gets called automatically by Decoder.tuple, but you may find it usefull for reordering tuple elements.

Example:

type Pair = [number, number];
 
const reverseTupleDecoder = Decoder.tuple<Pair>(
    Decoder.index(1, Decoder.int),
    Decoder.index(0, Decoder.int));
 
reverseTupleDecoder.decodeValue([3, 4]) // OK [4, 3]

Parameters:

Param Type
index number
indexDecoder Decoder<T>

Returns: Decoder<T>


<Static> guard

guard<T>(guard: (value: any) => value is T, message: string): Decoder<T>

Make a completely custom Decoder based on a type guard.


Mapping results

<Static> succeed

succeed<T>(value: T): Decoder<T>

Ignore the JSON and always return the given value instead. This is useful when used together with oneOf or andThen.

Parameters:

Param Type
value T

Returns: Decoder<T>


<Static> succeedOpt

succeedOpt<T>(value?: T): OptionalDecoder<T>

Decoder.succeed variant for optional fields. Ignore the JSON and always return the given value instead. This is useful when used together with oneOf or andThen.


<Static> fail

fail<T>(error: string): Decoder<T>

Ignore the JSON and always make the decoder fail with a custom error message. This is useful when used together with oneOf or andThen.

Parameters:

Param Type
error string

Returns: Decoder<T>


map

map<U>(transformer: function): Decoder<U>

Additionally map the decoded value to some other type after successfully decoding it.

Parameters:

Param Type
transformer function

Returns: Decoder<U>


filter

filter(message: string, predicate: function): Decoder<T>

After successfully decoding a value, additionally validate the resulting value using a predicate function. The decoder will also fail with message if this predicate returns false.

Example:

const pairDecoder = Decoder.array(Decoder.int)
    .filter("Expected a tuple [x, y] of exactly 2 elements!", value => value.length == 2)
 
pairDecoder.decodeString("[4, 3]") // Ok - [3,4]
pairDecoder.decodeString("[4, 3, 2]") // Error

Parameters:

Param Type
message string
predicate function

Returns: Decoder<T>


filterOptional

filterOptional<R>(this: Decoder<R | null | undefined>, message: string): Decoder<R>

Filter out null and undefined values. This returns a new Decoder that fails instead with the supplied message if it encounters one of those values. The returned Decoder no longer contains those values as possible success values, making this useful in combination with Decoder.function if you need to wrap a function returning null on a failure.

Parameters:

Param Type
message string

Returns: Decoder<R>


filterGuard

filterGuard<R>(guard: (value: T | R) => value is R, message: string): Decoder<T & R>

Filter out certain types of values using a type guard function.


orDefaultValue

orDefaultValue(defaultValue: T): Decoder<T>

Instead of failing, make a new decoder that instead returns a defaultValue if something goes wrong.

Example:

const int = Decoder.int;
const intOrDefault = int.orDefaultValue(0);
 
int.decodeValue(5)    // ==> 5
int.decodeValue(3.14) // ==> DecoderError
 
intOrDefault.decodeValue(5)      // ==> 5
intOrDefault.decodeValue(3.14)   // ==> 0

Parameters:

Param Type
defaultValue T

Returns: Decoder<T>


orElse

orElse<U>(alternative: Decoder<U>): Decoder< T | U>

Tries an alternative decoder on the same input if this one fails for some reason.

If you are building a big orElse chain, please note that if something goes wrong, the next decoder in the chain will always be tried, even though the current would "fit better". This is especially bad, because in a orElse chain, only the last decoder's errors will be returned. If you are decoding a discriminated union of some sort, consider switching to Decoder.andThen instead for better error messages and performance. see: Decoder.oneOf

Example:

interface Success<T> {
    value: T;
    success: true;
}
 
interface Failure {
    error: string;
    success: false;
}
 
type Result<T> = Success<T> | Failure;
 
const successDecoder = <T> (valueDecoder: Decoder<T>) => Decoder.obj<Success<T>>({
    value: valueDecoder,
    success: Decoder.tag(true)
});
 
const failureDecoder = Decoder.obj<Failure>({
    error: Decoder.string,
    success: Decoder.tag(false)
});
 
const resultDecoder = <T> (valueDecoder: Decoder<T>): Decoder<Result<T>> =>
    failureDecoder.orElse(successDecoder(valueDecoder));

Parameters:

Param Type
alternative Decoder<U>

Returns: Decoder< T | U>


<Static> oneOf

oneOf<T>(...decoders: Array<Decoder<T>>): Decoder<T>

Try all decoders in order until one of them succeeds. Note that because the next decoder will be tried if something goes wrong, only the error messages of the last decoder will be returned if all of them fail.

If you are dealing with versioned data or discriminated unions, consider switching to Decoder.andThen instead, to improve error messages and performance. see: Decoder.orElse

Parameters:

Param Type
Rest decoders Array<Decoder<T>>

Returns: Decoder<T>


andThen

andThen<U>(continuation: function): Decoder<U>

Runs a second Decoder on the same input as this one, but only if the first one succeeded. Using this, you can first parse a small part of a larger JSON structure to determine the actual Decoder to use. Possible applications might be versioned data, JSON with dynamic types, or tagged unions.

Example:

const configDecoder = Decoder.field('version', Decoder.int).andThen(version => {
    switch(version) {
        case 3:  return config3Decoder;
        case 4:  return config4Decoder;
        default: return Decoder.fail("Only Versions 3 and 4 are supported!");
    }
})

Parameters:

Param Type
continuation function

Returns: Decoder<U>


fold

fold<U>(Left: function, Right: function): Decoder<U>

Use an 'Either' type to pull errors into a successful value, making the Decoder no longer fail if something goes wrong, but instead call the Right constructor on the value.

Parameters:

Param Type Description
Left function 'Either' object constructor for the Failure case
Right function 'Either' object constructor for the Success case

Returns: Decoder<U>


Package Sidebar

Install

npm i elmish-decoder

Weekly Downloads

0

Version

2.0.2

License

BSD-3-Clause

Unpacked Size

119 kB

Total Files

17

Last publish

Collaborators

  • arkandos