ts-decoders

1.0.0 • Public • Published

NPM version Size when minified & gzipped Actively maintained

ts-decoders (demo)

This module facilitates validating/decoding an unknown input and returning a properly typed response (on success), or returning useful errors. It provides an assortment of decoder primatives which are usable as-is, easily customized, and composable with other decoders (including any custom decoders you may make).

import { assert } from 'ts-decoders';
import { numberD } from 'ts-decoders/decoders';

// assert() is a convenience function which wraps a decoder
const numberV = assert(numberD());

const value1 = numberV(1); // returns 1
const value2 = numberV('1'); // throws AssertDecoderError

alternatively

import { objectD, numberD } from 'ts-decoders/decoders';

const decoder = objectD({ count: numberD() });

// returns DecoderSuccess
const result = decoder.decode({ count: 1 });

const value = result.value; // { count: 1 }

// returns DecoderError
// message = 'invalid value for key ["count"] > must be a number'
decoder.decode({ count: '1' });
// returns DecoderError
// message = 'missing key ["count"]'
decoder.decode({ cont: 1 });
// returns DecoderError
// message = 'must be a non-null object'
decoder.decode(1);

Installation

yarn add ts-decoders
# or
npm install --save ts-decoders

Why this module is useful

ts-decoders greatly simplifies validation/decoding, allowing you to build up complex validation/decoding functions from composible parts. If you aren't familiar, there are number of decoder libraries available for typescript (see Similar projects). This library differentiates itself via:

  1. Fast and flexible method for creating custom error messages (see Working with errors).
    • The ability to return the first error or an array of all the errors.
  2. Full support for asyncronous decoders (see Working with promises).
  3. Class based Decoder interface with the ability to specify the decoder's input type in addition to it's return type (see Interfaces).

Usage

Basic usage

This module exports an assortment of primative decoder functions which each return a decoder. By convention, all of the exported decoder functions have the suffix D. For example, the numberD() function returns a Decoder<number, any> suitable for checking if an unknown value is a number.

import { areDecoderErrors } from 'ts-decoders';
import { numberD } from 'ts-decoders/decoders';

const myNumberDecoder = numberD();

function main() {
  const result = myNumberDecoder.decode(1); // returns a DecoderSuccess<number>
  const result = myNumberDecoder.decode('1'); // returns (not throws) a DecoderError

  if (areDecoderErrors(result)) {
    // do stuff...
    return;
  }

  // result is type `DecoderSuccess<number>
  // value is type `number`
  const value = result.value;
}

For your convenience, you can wrap any decoder with the exported assert function which will return a valid value directly or throw a AssertDecoderError.

const myNumberDecoder = assert(numberD());
const value = myNumberDecoder(1); // returns 1
const value = myNumberDecoder('1'); // will throw (not return) a AssertDecoderError

Some decoder functions aid with composing decoders.

const myNumberDecoder = undefinableD(numberD());
const result = myNumberDecoder.decode(1); // returns a DecoderSuccess<number | undefined>
const result = myNumberDecoder.decode(undefined); // returns a DecoderSuccess<number | undefined>
const result = myNumberDecoder.decode('1'); // returns DecoderError[]

A more complex example of decoder composition is the objectD() decoder function, which receives a {[key: string]: Decoder<R>} object argument. This argument is used to process a provided value: it verifies that the provided value is a non-null object, that the object has the specified keys, and that the values of the object's keys pass the provided decoder checks.

const myObjectDecoder = objectD({
  payload: objectD({
    values: arrayD( nullableD(numberD()) )
  })
})

const goodInput = { payload: { values: [0, null, 2] } } as unknown;

const success = myObjectDecoder.decode(goodInput); // will return `DecoderSuccess`

// Notice that success.value has the type
type SuccessValue = { payload: string; { values: Array<number | null> } }

const badInput = { payload: { values: [0, null, '1'] } } as unknown;

const errors = myObjectDecoder.decode(badInput); // will return `DecoderError[]`

errors.get(0)!.message // invalid value for key ["payload"] > invalid value for key ["values"] > invalid element [2] > must be a number or must be null

Interfaces

This module exports two base decoder classes Decoder<R, I> and AsyncDecoder<R, I>. It also exports a base DecoderSuccess<T> class and DecoderError class.

Decoder<R, I>

The first type argument, R, contains the successful return type of the decoder. The second type argument, I, contains the type of the input argument passed to the decoder.

class Decoder<R, I = any> {
  /** The internal function this decoder uses to decode values */
  decodeFn: (value: I) => DecodeFnResult<R>;

  constructor(decodeFn: (value: I) => DecodeFnResult<R>): Decoder<R, I>;

  /**
   * Decodes a value of type `Promise<I>` and returns
   * a `Promise<DecoderResult<R>>`.
   */
  decode(value: Promise<I>): Promise<DecoderResult<R>>;
  /**
   * Decodes a value of type `I` and returns a `DecoderResult<R>`.
   */
  decode(value: I): DecoderResult<R>;

  /**
   * On decode failure, handle the DecoderErrors.
   */
  catch<K>(
    fn: (input: unknown, errors: DecoderError[]) => DecodeFnResult<K>,
  ): Decoder<K | R, I>;

  /**
   * On decode success, transform a value using a provided transformation function.
   */
  map<K>(fn: (value: R) => K): Decoder<K, I>;

  /**
   * On decode success, perform a new validation check.
   */
  chain<K>(fn: (input: R) => DecodeFnResult<K>): Decoder<K, I>;
  chain<K>(decoder: Decoder<K, R>): Decoder<K, I>;

  toAsyncDecoder(): AsyncDecoder<R, I>;
}

AsyncDecoder<R, I>

The first type argument, R, contains the successful return type of the decoder. The second type argument, I, contains the type of the input argument passed to the decoder.

class AsyncDecoder<R, I = any> {
  /** The internal function this decoder uses to decode values */
  readonly decodeFn: (input: I) => Promise<DecodeFnResult<R>>;

  constructor(
    decodeFn: (value: I) => Promise<DecodeFnResult<R>>,
  ): AsyncDecoder<R, I>;

  /**
   * Decodes a value (or promise returning a value) of type `I`
   * and returns a `Promise<DecoderResult<R>>`
   */
  decode(value: I | Promise<I>): Promise<DecoderResult<R>>;

  /**
   * On decode failure, handle the DecoderErrors.
   */
  catch<K>(
    fn: (
      input: unknown,
      errors: DecoderError[],
    ) => DecodeFnResult<K> | Promise<DecodeFnResult<K>>,
  ): AsyncDecoder<K | R, I>;

  /**
   * On decode success, transform a value using a provided transformation function.
   */
  map<K>(fn: (value: R) => K | Promise<K>): AsyncDecoder<K, I>;

  /**
   * On decode success, perform a new validation check.
   */
  chain<K>(
    fn: (input: R) => DecodeFnResult<K> | Promise<DecodeFnResult<K>>,
  ): AsyncDecoder<K, I>;
  chain<K>(decoder: Decoder<K, R> | AsyncDecoder<K, R>): AsyncDecoder<K, I>;
}

DecoderSuccess

class DecoderSuccess<T> {
  constructor(value: T): DecoderSuccess<T>;

  value: T;
}

DecoderError

class DecoderError {
  /** The input that failed validation. */
  input: any;

  /** The type of error. */
  type: string;

  /** A human readable error message. */
  message: string;

  /** The name of the decoder which created this error. */
  decoderName: string;

  /**
   * A human readable string showing the nested location of the error.
   * If the validation error is not nested, location will equal a blank string.
   */
  location: string;

  /** The `DecoderError` which triggered this `DecoderError`, if any */
  child?: DecoderError;

  /**
   * The key associated with this `DecoderError` if any.
   *
   * - example: this could be the index of the array element which
   *   failed validation.
   */
  key?: any;

  /** Convenience property for storing arbitrary data. */
  data: any;

  constructor(
    input: any,
    type: string,
    message: string,
    options?: {
      decoderName?: string;
      location?: string;
      child?: DecoderError;
      key?: any;
      data?: any;
    },
  ): DecoderError;

  /**
   * Starting with this error, an array of the keys associated with
   * this error as well as all child errors.
   */
  path(): any[];
}

DecoderResult

type DecoderResult<T> = DecoderSuccess<T> | DecoderError[];

DecodeFnResult

type DecodeFnResult<T> = DecoderSuccess<T> | DecoderError | DecoderError[];

DecoderErrorMsgArg

type DecoderErrorMsgArg =
  | string
  | ((input: any, errors: DecoderError[]) => DecoderError | DecoderError[]);

Working with errors

see the DecoderError interface

The errors API is designed to facilitate custom human and machine readable errors.

allErrors option

By default, (most) decoders will immediately return the first error they encounter. If you pass the allErrors: true" option when calling a decoder function, then the returned decoder will instead process and return all errors from an input value (or DecoderSuccess). These errors will be returned as an array.

decoderName option

DecoderError objects have an optional decoderName: string property which can be useful for easily identifying what decoder an error came from. All of the decoder functions in this module add a decoderName to their error messages. By passing the decoderName: string option when calling a decoder function, you can change the decoderName associated with a decoder's errors.

errorMsg option

see the DecoderErrorMsgArg type

If you wish to customize the error message(s) a decoder returns, you can pass the errorMsg: DecoderErrorMsgArg option when calling a decoder function.

If you pass a string as the errorMsg option, that string will be used as the error message for that decoder.

Example:

const myObjectDecoder = objectD({
  payload: objectD({
    values: arrayD(nullableD(numberD()), { errorMsg: 'very bad array!' }),
  }),
});

const badInput = { payload: { values: [0, null, '1'] } } as unknown;

const errors = myObjectDecoder.decode(badInput); // will return `DecoderError[]`

errors[0].message; // "invalid value for key \"payload\" > invalid value for key \"values\" > very bad array!"
errors[0].location; // "payload.values"
errors[0].path(); // ["payload", "values"]
errors[0].child.message; // "invalid value for key \"values\" > very bad array!"
errors[0].child.child.message; // "very bad array!"
errors[0].child.child.child.message; // "must be a string"
errors[0].child.child.child.child; // undefined

For more control over your error messages, you can provide a (error: DecoderError[]) => DecoderError | DecoderError[] function as the errorMsg option.

If one or more DecoderError occur, the errors will be passed to the provided errorMsg function where you can either manipulate the errors or return new errors. Your function must return at least one DecoderError.

Example:

const errorMsgFn = (input: any, errors: DecoderError[]) => {
  errors.forEach(error => {
    const { decoderName } = error.child;

    if (decoderName !== 'arrayD') {
      error.message = 'array must have a length of 2';
    } else if (error.child.child) {
      error.message = 'must be an array of numbers';
    } else {
      error.message = 'must be an array';
    }
  });

  return errors;
};

const LAT_LONG_DEC = chainOfD(
  arrayD(numberD()),
  predicateD(input => input.length === 2),
  { decoderName: 'latLongDecoder', errorMsg: errorMsgFn },
);

const badInput = [1] as unknown;

const errors = LAT_LONG_DEC.decode(badInput);

errors[0].message; // "array must have a length of 2"

Creating custom decoders

There are a two ways of creating custom decoders. This simplest way is to simply compose multiple decoders together. For example, the following latitude and longitude decoder is created by composing arrayD(numberD()) and predicateD() using chainOfD();

const LAT_LONG_DEC = chainOfD(
  arrayD(numberD()),
  predicateD(input => input.length === 2),
  { decoderName: 'latLongDecoder' },
);

For more flexibility, you can create a new decoder from scratch using either the Decoder or AsyncDecoder constructors (see the working with promises section for a description of the differences between Decoder and AsyncDecoder). To make a new decoder from scratch, simply pass a custom decode function to the Decoder constructor. A decode function is a function which receives a value and returns a DecodeSuccess object on success or a DecoderError | DecoderError[] on failure.

Example:

const myCustomDecoder = new Decoder(input =>
  typeof input === 'boolean'
    ? new DecoderSuccess(input)
    : new DecoderError(input, 'invalid type', 'must be a boolean'),
);

// You can then compose this decoder with others normally

objectD({ likesTsDecoders: myCustomDecoder });

// Or use it directly

myCustomDecoder.decode(true);

Specifying an input value type

While most (all?) of the decoders shipped with this library expect an unknown input value, it is possible to create a decoder which requires an already typed input value. The I type arg in Decoder<R, I> is the input variable type (the default is any). To create a decoder which requires an input value to be of a specific type, simply type the input of the decoder's decodeFn.

Example:

const arrayLengthDecoder = new Decoder((input: any[]) =>
  input.length < 100
    ? new DecoderSuccess(input)
    : new DecoderError(
        input,
        'invalid length',
        'must have length less than 100',
      ),
);

arrayLengthDecoder.decode(1); // type error! decode() expects an array

This decoder only works on array values. One use case for a decoder like this is inside the chainOfD() decoder, after we have already verified that a value is an array.

Example:

chainOfD(
  arrayD(),
  arrayLengthDecoder, // <-- this will only be called when the value is an array
);

Creating custom decoder functions

Like this module, you may wish to create custom decoder functions (e.g. objectD()) to dynamically compose decoders together or to help create new decoders. It's recommended that, before doing so, you familiarize yourself with the conventions used by this module.

  1. If your function allows users to pass options to it, in general those options should all go into an optional options object which is the last argument to the function.
    • An exception to this recommendation would be the dictionaryD() function, which can accept an optional key decoder as the second argument and an options object as the third argument. In this case, typescript overloads are used to keep the API friendly.
  2. If appropriate, allow users to customize the returned errors by passing a errorMsg?: DecoderErrorMsgArg option.
  3. If your decoder may return multiple DecoderError, immediately return the first error by default. Allow users to pass an allErrors: true option to return multiple errors.
  4. If your function takes one or more decoders as an argument, you need to handle the possibility of being passed a AsyncDecoder. If you receive one or more AsyncDecoders, your composition function should return a AsyncDecoder. Typescript overloads can be used to properly type the different returns.
  5. This module exports various utilities that can simplify the process of creating custom decoder functions.

Working with promises

Every decoder supports calling its decode() method with a promise which returns the value to be decoded. In this scenerio, decode() will return a Promise<DecoderResult<T>>. Internally, the decoder will wait for the promise to resolve before passing the value to its decodeFn. As such, the internal decodeFn will never be passed a promise value.

If you wish to create a custom decoder with a decodeFn which returns a promise, then you must use the AsyncDecoder class. AsyncDecoder is largely identical to Decoder, except its decode() method always returns Promise<DecoderResult<T>> (not just when called with a promise value) and it's decodeFn returns a promise. Additionally, when calling a decoder function with a AsyncDecoder and allErrors: true arguments, many decoder functions will process input values in parallel rather than serially.

As an example, calling objectD() or arrayD() with a AsyncDecoder and allErrors: true will create a decoder which decodes each key in parallel.

declare class PersonService {
  checkIfIdExists(id: string): Promise<boolean>;
}

declare const personService: PersonService;

const personIdDecoder = chainOfD(
  uuidD(),
  new AsyncDecoder(async id => {
    const result = await personService.checkIfIdExists(id);

    if (result) return new DecoderSuccess(id);

    return new DecoderError(id, 'invalid id', 'the provided id does not exist');
  }),
);

const myObjectDecoder = objectD(
  {
    type: stringD(),
    payload: objectD({
      values: arrayD(nullableD(personIdDecoder)),
    }),
  },
  { allErrors: true },
);

// when you compose Decoders with a AsyncDecoder,
// the result is a AsyncDecoder
myObjectDecoder instanceof AsyncDecoder === true;

Tips and tricks

The assert() function

It may be the case that you simply want to return the validated value from a decoder directly, rather than a DecoderResult. In this case, wrap a decoder with assert() to get a callable function which will return a valid value on success, or throw (not return) a AssertDecoderError on failure.

Example:

const validator = assert(numberD());

const value = validator(1); // returns 1

const value = validator('1'); // will throw a `AssertDecoderError`

The decoder map() method

Decoders have a map method which can be used to transform valid input values. For example, say you are receiving a date param in the form of a string, and you want to convert it to a javascript Date object. The following decoder will verify that a string is in a YYYY-MM-DD format and, if so, convert the string to a date.

const stringDateDecoder =
  // this regex verifies that a string is of the form `YYYY-MM-DD`
  matchD(/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/).map(
    value => new Date(value),
  );

const result = stringDateDecoder.decode('2000-01-01'); // returns `DecoderSuccess<Date>`

if (result instanceof DecoderSuccess) {
  const value: Date = result.value;
}

The decoder chain() method

also see the chainOfD decoder for similar functionality

Decoders have a chain method which can be used to chain on an additional validation check if the first one was a success. Building off the example for map above, say you've made a stringDateDecoder which recieves a string and converts it to a javascript Date object if the string has the form YYYY-MM-DD. You decide that you want to perform an additional validation check on the Date object to make sure that the date is after the year 2000.

const afterYear2000Decoder = stringDateDecoder.chain(date => {
  if (date.valueOf() > new Date(2000)) {
    return new DecoderSuccess(date);
  }

  return new DecoderError(date, 'invalid-date', 'must be after the year 2000');
});

const result = afterYear2000Decoder.decode('1998-01-01'); // returns `[DecoderError]`

Getting a type equal to the return type of a decoder

For convenience, you may want to generate a typescript type which is equal to the return type of a decoder. For example, if you create a decoder for some web request params, you might also like to separately have a type representing these web request params that you can use in type assertions. You can use the DecoderReturnType<T> and DecoderInputType<T> types for this.

For example:

const textDec = chainOfD(
  stringD(),
  predicateD(input => input.length > 1, {
    errorMsg: 'must be 2 or more characters',
  }),
);

const paramsDec = objectD({
  __typename: exactlyD('Person'),
  id: uuidD(),
  firstName: textDec,
  middleName: textDec,
  lastName: textDec,
  address: objectD({
    __typename: exactlyD('Address'),
    street: textDec,
    city: textDec,
  }),
});

type Params = DecoderReturnType<typeof paramsDec>;

// here, the Params type is equal to
//
// interface Params {
//   __typename: "Person",
//   id: string,
//   firstName: string,
//   middleName: string,
//   lastName: string,
//   address: {
//     __typename: "Address",
//     street: string,
//     city: string,
//   }
// }

Decoder API

assert()

assert() accepts a single decoder as an argument and returns a new function which can be used to decode the same values as the provided decoder. On decode success, the validated value is returned directly and on failure the AssertDecoderError is thrown (rather than returned).

Interface:

class AssertDecoderError extends Error {
  errors: DecoderError[];

  constructor(errors: DecoderError[]): AssertDecoderError;
}

function assert<R, V>(
  decoder: Decoder<R, V>,
): { (value: V): R; (value: Promise<V>): Promise<R> };
function assert<R, V>(
  decoder: AsyncDecoder<R, V>,
): (value: V | Promise<V>) => Promise<R>;

Example:

const validator = assert(numberD());

const value = validator(1); // returns 1;

const value = validator('1'); // will throw a `AssertDecoderError`

anyD()

anyD() creates a decoder which always returns DecoderSuccess with whatever input value is provided to it.

Interface:

function anyD<T = any>(): Decoder<T, any>;

Example:

// Decoder<any>;
const decoder1 = anyD();

// Decoder<string>;
const decoder2 = anyD<string>();

anyOfD()

anyOfD() accepts an array of decoders and attempts to decode a provided value using each of them, in order, returning the first successful result or DecoderError[] if all fail. Unlike other decoder functions, anyOfD() always returns all errors surfaced by it's decoder arguments. By default, decoder arguments are tried in the order they are given.

Async: when calling anyOfD() with one or more AsyncDecoder arguments, you can pass a decodeInParallel: true option to specify that a provided value should be tried against all decoder arguments in parallel.

Interface:

interface AnyOfDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
  decodeInParallel?: boolean;
}

function anyOfD<T extends Decoder<unknown>>(
  decoders: T[],
  options?: AnyOfDecoderOptions,
): Decoder<DecoderReturnType<T>>;
function anyOfD<T extends Decoder<unknown> | AsyncDecoder<unknown>>(
  decoders: T[],
  options?: AnyOfDecoderOptions,
): AsyncDecoder<DecoderReturnType<T>>;

Example:

// Decoder<string | number>;
const decoder = anyOfD(stringD(), numberD());

arrayD()

arrayD() can be used to make sure an input is an array. If an optional decoder argument is provided, that decoder will be used to process all of the input's elements.

Options:

  • If you pass an allErrors: true option as well as any AsyncDecoders as arguments, then arrayD() will create a new AsyncDecoder which decodes each index of the input array in parallel.

Related:

Interface:

interface ArrayDecoderOptions {
  decoderName?: string;
  allErrors?: boolean;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function arrayD<R = any>(options?: ArrayDecoderOptions): Decoder<R[]>;
function arrayD<R>(
  decoder: Decoder<R>,
  options?: ArrayDecoderOptions,
): Decoder<R[]>;
function arrayD<R>(
  decoder: AsyncDecoder<R>,
  options?: ArrayDecoderOptions,
): AsyncDecoder<R[]>;

Example:

// Decoder<string[]>;
const decoder = arrayD(stringD());

booleanD()

booleanD() can be used to verify that an unknown value is a boolean.

Interface:

interface BooleanDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function booleanD(options?: BooleanDecoderOptions): Decoder<boolean, any>;

Example:

// Decoder<boolean>;
const decoder = booleanD();

chainOfD()

chainOfD() accepts a spread or array of decoders and attempts to decode a provided value using all of them, in order. The successful output of one decoder is provided as input to the next decoder. chainOfD() returns the DecoderSuccess value of the last decoder in the chain or DecoderError on the first failure. Alternate names for this decoder could be: pipe, compose, or allOf.

Related:

Interface:

interface ChainOfDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

export function chainOfD<A, B, C, D, E, F, G>(
  a: Decoder<B, A>,
  b: Decoder<C, B>,
  c: Decoder<D, C>,
  d: Decoder<E, D>,
  e: Decoder<F, E>,
  f: Decoder<G, F>,
  options?: ChainOfDecoderOptions,
): Decoder<G, A>;
export function chainOfD<A, B, C, D, E, F>(
  a: Decoder<B, A>,
  b: Decoder<C, B>,
  c: Decoder<D, C>,
  d: Decoder<E, D>,
  e: Decoder<F, E>,
  options?: ChainOfDecoderOptions,
): Decoder<F, A>;
export function chainOfD<A, B, C, D, E>(
  a: Decoder<B, A>,
  b: Decoder<C, B>,
  c: Decoder<D, C>,
  d: Decoder<E, D>,
  options?: ChainOfDecoderOptions,
): Decoder<E, A>;
export function chainOfD<A, B, C, D>(
  a: Decoder<B, A>,
  b: Decoder<C, B>,
  c: Decoder<D, C>,
  options?: ChainOfDecoderOptions,
): Decoder<D, A>;
export function chainOfD<A, B, C>(
  a: Decoder<B, A>,
  b: Decoder<C, B>,
  options?: ChainOfDecoderOptions,
): Decoder<C, A>;
export function chainOfD<A, B>(
  a: Decoder<B, A>,
  options?: ChainOfDecoderOptions,
): Decoder<B, A>;
export function chainOfD<A, B = any>(
  decoders: [Decoder<any, A>, ...Array<Decoder<any>>],
  options?: ChainOfDecoderOptions,
): Decoder<B, A>;
export function chainOfD<A, B, C, D, E, F, G>(
  a: Decoder<B, A> | AsyncDecoder<B, A>,
  b: Decoder<C, B> | AsyncDecoder<C, B>,
  c: Decoder<D, C> | AsyncDecoder<D, C>,
  d: Decoder<E, D> | AsyncDecoder<E, D>,
  e: Decoder<F, E> | AsyncDecoder<F, E>,
  f: Decoder<G, F> | AsyncDecoder<G, F>,
  options?: ChainOfDecoderOptions,
): AsyncDecoder<G, A>;
export function chainOfD<A, B, C, D, E, F>(
  a: Decoder<B, A> | AsyncDecoder<B, A>,
  b: Decoder<C, B> | AsyncDecoder<C, B>,
  c: Decoder<D, C> | AsyncDecoder<D, C>,
  d: Decoder<E, D> | AsyncDecoder<E, D>,
  e: Decoder<F, E> | AsyncDecoder<F, E>,
  options?: ChainOfDecoderOptions,
): AsyncDecoder<F, A>;
export function chainOfD<A, B, C, D, E>(
  a: Decoder<B, A> | AsyncDecoder<B, A>,
  b: Decoder<C, B> | AsyncDecoder<C, B>,
  c: Decoder<D, C> | AsyncDecoder<D, C>,
  d: Decoder<E, D> | AsyncDecoder<E, D>,
  options?: ChainOfDecoderOptions,
): AsyncDecoder<E, A>;
export function chainOfD<A, B, C, D>(
  a: Decoder<B, A> | AsyncDecoder<B, A>,
  b: Decoder<C, B> | AsyncDecoder<C, B>,
  c: Decoder<D, C> | AsyncDecoder<D, C>,
  options?: ChainOfDecoderOptions,
): AsyncDecoder<D, A>;
export function chainOfD<A, B, C>(
  a: Decoder<B, A> | AsyncDecoder<B, A>,
  b: Decoder<C, B> | AsyncDecoder<C, B>,
  options?: ChainOfDecoderOptions,
): AsyncDecoder<C, A>;
export function chainOfD<A, B>(
  a: Decoder<B, A> | AsyncDecoder<B, A>,
  options?: ChainOfDecoderOptions,
): AsyncDecoder<B, A>;
export function chainOfD<A, B = any>(
  decoders: [
    Decoder<any, A> | AsyncDecoder<any, A>,
    ...Array<Decoder<any> | AsyncDecoder<any>>,
  ],
  options?: ChainOfDecoderOptions,
): AsyncDecoder<B, A>;

Example:

// Decoder<string[]>;
const decoder = chainOfD(
  arrayD(stringD()),
  predicateD(input => input.length === 2),
);

constantD()

constantD() accepts a value: T argument and creates a decoder which always returns DecoderSuccess<T> with the provided value argument, ignoring its input.

Interface:

function constantD<T extends string | number | bigint | boolean>(
  exact: T,
): Decoder<T, any>;
function constantD<T>(value: T): Decoder<T, any>;

Example:

// Decoder<number>;
const decoder = constantD(13);

dictionaryD()

dictionaryD() receives a decoder argument and uses that decoder to process all values (regardless of key) of an input object. You can pass an optional key decoder as the second argument which will be used to decode each key of an input object.

Options:

  • If you pass an allErrors: true option as well as any AsyncDecoders as arguments, then dictionaryD() will create a new AsyncDecoder which decodes each key of the input object in parallel.

Related:

Interface:

interface DictionaryDecoderOptions {
  decoderName?: string;
  allErrors?: boolean;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function dictionaryD<R, V>(
  valueDecoder: Decoder<R, V>,
  options?: DictionaryDecoderOptions,
): Decoder<{ [key: string]: R }, V>;
function dictionaryD<R, V>(
  valueDecoder: Decoder<R, V>,
  keyDecoder: Decoder<string, string>,
  options?: DictionaryDecoderOptions,
): Decoder<{ [key: string]: R }, V>;
function dictionaryD<R, V>(
  decoder: AsyncDecoder<R, V>,
  options?: DictionaryDecoderOptions,
): AsyncDecoder<{ [key: string]: R }, V>;
function dictionaryD<R, V>(
  valueDecoder: Decoder<R, V> | AsyncDecoder<R, V>,
  keyDecoder: Decoder<string, string> | AsyncDecoder<string, string>,
  options?: DictionaryDecoderOptions,
): AsyncDecoder<{ [key: string]: R }, V>;

Example:

// Decoder<{[key: string]: string}>;
const decoder1 = dictionaryD(stringD());

// Decoder<{[key: string]: number}>;
const decoder2 = dictionaryD(numberD(), predicateD(input => input.length < 5));

emailD()

emailD() can be used to verify that an unknown value is an email address string.

Interface:

interface EmailDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function emailD(options?: EmailDecoderOptions): Decoder<string, any>;

Example:

// Decoder<string>;
const decoder = emailD();

exactlyD()

exactlyD() accepts a value argument and can be used to verify that an unknown input is === value.

Interface:

interface ExactlyDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function exactlyD<T extends string | number | bigint | boolean>(
  exact: T,
  options?: ExactlyDecoderOptions,
): Decoder<T>;
function exactlyD<T>(exact: T, options?: ExactlyDecoderOptions): Decoder<T>;

Example:

// Decoder<'one'>;
const decoder = exactlyD('one');

instanceOfD()

instanceOfD() accepts a javascript constructor argument and creates a decoder which verifies that its input is instanceof clazz.

Interface:

interface InstanceOfDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function instanceOfD<T extends new (...args: any) => any>(
  clazz: T,
  options?: InstanceOfDecoderOptions,
): Decoder<InstanceType<T>, any>;

Example:

// Decoder<Map>;
const decoder = instanceOfD(Map);

integerD()

integerD() can be used to verify that an unknown value is a whole number.

Related:

Interface:

interface IntegerDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function integerD(options?: IntegerDecoderOptions): Decoder<number, any>;

Example:

// Decoder<number>;
const decoder = integerD();

lazyD()

lazyD() recieves a function which returns a decoder and creates a new decoder which calls this function on each .decode() call and uses the returned decoder to decode it's input. A common use case for this decoder is to decode recursive data structures. Alternate names for this decoder could be: recursive.

Interface:

interface LazyDecoderOptions {
  decoderName?: string;
  promise?: boolean;
}

export function lazyD<R, I = any>(
  decoderFn: (value: I) => Decoder<R, I>,
  options?: LazyDecoderOptions & { promise?: false },
): Decoder<R, I>;
export function lazyD<R, I = any>(
  decoderFn: (
    value: I,
  ) =>
    | Decoder<R, I>
    | AsyncDecoder<R, I>
    | Promise<Decoder<R, I> | AsyncDecoder<R, I>>,
  options: LazyDecoderOptions & { promise: true },
): AsyncDecoder<R, I>;

Example:

interface ArrayLike {
  [key: number]: ArrayLike;
}

// Decoder<ArrayLike>
const decoder1 = arrayD(lazyD(() => decoder));

// Decoder<string | number>
const decoder2 = lazyD<string | number>(input =>
  typeof input === 'number' ? integerD() : stringD(),
);

matchD()

matchD() can be used to verify that an unknown value is a string which conforms to the given RegExp.

Interface:

interface MatchDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function matchD(
  regex: RegExp,
  options?: MatchDecoderOptions,
): Decoder<string, any>;

Example:

// Decoder<Date>
const decoder =
  // this regex verifies that a string is of the form `YYYY-MM-DD`
  matchD(/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/).map(
    value => new Date(value),
  );

neverD()

neverD() creates a decoder which always returns DecoderError with whatever input value is provided to it.

Interface:

interface NeverDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function neverD(options?: NeverDecoderOptions): Decoder<never, any>;

Example:

// Decoder<never>
const decoder = neverD();

nullableD()

nullableD() accepts a decoder and returns a new decoder which accepts either the original decoder's value or null.

Related:

Interface:

interface NullableDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function nullableD<R, I>(
  decoder: Decoder<R, I>,
  options?: NullableDecoderOptions,
): Decoder<R | null, I>;
function nullableD<R, I>(
  decoder: AsyncDecoder<R, I>,
  options?: NullableDecoderOptions,
): AsyncDecoder<R | null, I>;

Example:

// Decoder<string | null>
const decoder = nullableD(stringD());

numberD()

numberD() can be used to verify that an unknown value is a number.

Related:

Interface:

interface NumberDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function numberD(options?: NumberDecoderOptions): Decoder<number, any>;

Example:

// Decoder<number>
const decoder = numberD();

objectD()

"Object element" refers to a key: value pair of the object. "Element-value" refers to the value of this pair and "element-key" refers to the key of this pair

objectD() accepts a {[key: string]: Decoder<any> | AsyncDecoder<any>} init object argument and returns a new decoder that will verify that an input is a non-null object, and that each element-key of the input is decoded by the corresponding element-key of the init object. On DecoderSuccess, a new object is returned which has element-values defined by the init object's element-values. By default, any excess properties on the input object are ignored (i.e. not included on the returned value).

Options:

  • If you pass the noExcessProperties: true option, any excess properties on the input object will return a DecoderError.
  • If you pass an allErrors: true option as well as any AsyncDecoders as arguments, then objectD() will create a new AsyncDecoder which decodes each key of the input object in parallel.
  • If you pass the removeUndefinedProperties: true option, then after all other decoding of an input succeeds, any undefined properties are deleted from the result.

Related:

Interface:

interface ObjectDecoderOptions {
  decoderName?: string;
  allErrors?: boolean;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
  noExcessProperties?: boolean;
  removeUndefinedProperties?: boolean;
}

function objectD<T>(
  decoderObject: { [P in keyof T]: Decoder<T[P]> },
  options?: ObjectDecoderOptions,
): Decoder<T>;
function objectD<T>(
  decoderObject: { [P in keyof T]: Decoder<T[P]> | AsyncDecoder<T[P]> },
  options?: ObjectDecoderOptions,
): AsyncDecoder<T>;

Example:

// Decoder<{one: string; two: number}>
const decoder1 = objectD({
  one: stringD(),
  two: numberD(),
});

// AsyncDecoder<{
//   one: string;
//   two: {
//     three: number;
//     four: string;
//   };
// }>
const decoder2 = objectD({
  one: stringD(),
  two: objectD({
    three: numberD(),
    four: chainOf(
      string(),
      predicateD(input => Promise.resolve(true), {
        promise: true,
      }),
    ),
  }),
});

optionalD()

optionalD() accepts a decoder and returns a new decoder which accepts either the original decoder's value or null or undefined.

Related:

Interface:

interface OptionalDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function optionalD<R, I>(
  decoder: Decoder<R, I>,
  options?: OptionalDecoderOptions,
): Decoder<R | null | undefined, I>;
function optionalD<R, I>(
  decoder: AsyncDecoder<R, I>,
  options?: OptionalDecoderOptions,
): AsyncDecoder<R | null | undefined, I>;

Example:

// Decoder<string | null | undefined>
const decoder = optionalD(stringD());

predicateD()

predicateD() accepts a predicate function argument and creates a decoder which verifies that inputs pass the function check.

Async: to pass a predicate function which returns a promise resolving to a boolean, pass the promise: true option to predicateD().

Interface:

interface PredicateDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
  promise?: boolean;
}

function predicateD<T>(
  fn: (value: T) => boolean | Promise<boolean>,
  options: PredicateDecoderOptions & { promise: true },
): AsyncDecoder<T, T>;
function predicateD<T>(
  fn: (value: T) => boolean,
  options?: PredicateDecoderOptions,
): Decoder<T, T>;

Example:

// Decoder<string, string>
const decoder = predicateD((input: string) => input.length > 5, {
  errorMsg: 'must have length greater than 5',
});

stringD()

stringD() can be used to verify that an unknown value is a string.

Interface:

interface StringDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function stringD(options?: StringDecoderOptions): Decoder<string, any>;

Example:

// Decoder<string>
const decoder = stringD();

tupleD()

tupleD() receives an array of decoders and creates a decoder which can be used to verify that an input is:

  1. An array of the same length as the decoder argument array.
  2. The first decoder argument will be used the process the first element of an input array.
  3. The second decoder argument will be used the process the second element of an input array.
  4. etc...

Options:

  • If you pass an allErrors: true option as well as any AsyncDecoders as arguments, then tupleD() will create a new AsyncDecoder which decodes each index of the input array in parallel.

Related:

Interface:

interface TupleDecoderOptions {
  decoderName?: string;
  allErrors?: boolean;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function tupleD<R extends [unknown, ...unknown[]]>(
  decoders: { [P in keyof R]: Decoder<R[P]> },
  options?: TupleDecoderOptions,
): Decoder<R>;

function tupleD<R extends [unknown, ...unknown[]]>(
  decoders: { [P in keyof R]: Decoder<R[P]> | AsyncDecoder<R[P]> },
  options?: TupleDecoderOptions,
): AsyncDecoder<R>;

Example:

// Decoder<[string, string]>
const decoder = tupleD([stringD(), uuidD()]);

undefinableD()

undefinableD() accepts a decoder and returns a new decoder which accepts either the original decoder's value or undefined.

Related:

Interface:

interface UndefinableDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function undefinableD<R, I>(
  decoder: Decoder<R, I>,
  options?: UndefinableDecoderOptions,
): Decoder<R | undefined, I>;
function undefinableD<R, I>(
  decoder: AsyncDecoder<R, I>,
  options?: UndefinableDecoderOptions,
): AsyncDecoder<R | undefined, I>;

Example:

// Decoder<string | undefined>
const decoder = undefinableD(stringD());

uuidD()

uuidD() can be used to verify that an unknown value is a uuid v4 string.

Interface:

interface UUIDDecoderOptions {
  decoderName?: string;
  errorMsg?: DecoderErrorMsgArg;
  data?: any;
}

function uuidD(options?: UUIDDecoderOptions): Decoder<string, any>;

Example:

// Decoder<string>
const decoder = uuidD();

Similar projects

This repo has been inspired by a number of other decoder / validation libraries. If you are interested in functional programming, it is highly recommended you take a look at io-ts which has a functional design.

Package Sidebar

Install

npm i ts-decoders

Weekly Downloads

139

Version

1.0.0

License

Unlicense

Unpacked Size

268 kB

Total Files

83

Last publish

Collaborators

  • jcarroll