Decoder<T> methods

All decoders have the following methods.


# .verify(blob: mixed): T (source)

Verifies the untrusted/unknown input and either accepts or rejects it. When accepted, returns a value of type T. Otherwise fail with a runtime error.

The .verify() method explained

For example, take this simple number decoder.

// πŸ‘
number.verify(123);     // 123
number.verify(3.1415);  // 3.1415

// πŸ‘Ž
number.verify('hello'); // throws
// Decoding error:
// "hello"
// ^^^^^^^ Must be number

# .value(blob: mixed): T | undefined (source)

Verifies the untrusted/unknown input and either accepts or rejects it. When accepted, returns the decoded T value directly. Otherwise returns undefined.

Use this when you’re not interested in programmatically handling the error message.

The .value() method explained

// πŸ‘
number.value(3);     // 3
string.value('hi');  // 'hi'

// πŸ‘Ž
number.value('hi');  // undefined
string.value(42);    // undefined

NOTE: When you use this on optional() decoders, you cannot distinguish a rejected value from an accepted undefined input value.


# .decode(blob: mixed): DecodeResult<T> (source)

Verifies the untrusted/unknown input and either accepts or rejects it.

Contrasted with .verify(), calls to .decode() will never fail and instead return a result type.

The .decode() method explained

For example, take this simple β€œnumber” decoder. When given an number value, it will return an ok: true result. Otherwise, it will return an ok: false result with the original input value annotated.

// πŸ‘
number.decode(3);     // { ok: true, value: 3 };

// πŸ‘Ž
number.decode('hi');  // { ok: false, error: { type: 'scalar', value: 'hi', text: 'Must be number' } }

# .transform<V>(transformFn: (T) => V): Decoder<V> (source)

Accepts any value the given decoder accepts, and on success, will call the given function on the decoded result. If the transformation function throws an error, the whole decoder will fail using the error message as the failure reason.

const upper = string.transform((s) => s.toUpperCase());

// πŸ‘
upper.verify('foo') === 'FOO'

// πŸ‘Ž
upper.verify(4);  // throws

# .refine(predicate: T => boolean, message: string): Decoder<T> (source)

Adds an extra predicate to a decoder. The new decoder is like the original decoder, but only accepts values that also meet the predicate.

const odd = number.refine(
  (n) => n % 2 !== 0,
  'Must be odd'
);

// πŸ‘
odd.verify(3) === 3;

// πŸ‘Ž
odd.verify(42);    // throws: not an odd number
odd.verify('hi');  // throws: not a number

In TypeScript, if you provide a predicate that also is a type predicate, then this will be reflected in the return type, too.


# .reject(rejectFn: T => string | null): Decoder<T> (source)

Adds an extra predicate to a decoder. The new decoder is like the original decoder, but only accepts values that aren’t rejected by the given function.

The given function can return null to accept the decoded value, or return a specific error message to reject.

Unlike .refine(), you can use this function to return a dynamic error message.

const decoder = pojo
  .reject(
    obj => {
      const badKeys = Object.keys(obj).filter(key => key.startsWith('_'));
      return badKeys.length > 0
        ? `Disallowed keys: ${badKeys.join(', ')}`
        : null;
    }
  );

// πŸ‘
decoder.verify({ id: 123, name: 'Vincent' }) === { id: 123, name: 'Vincent' };

// πŸ‘Ž
decoder.verify({ id: 123, _name: 'Vincent'  })   // throws: "Disallowed keys: _name"

# .describe(message: string): Decoder<T> (source)

Uses the given decoder, but will use an alternative error message in case it rejects. This can be used to simplify or shorten otherwise long or low-level/technical errors.

const vowel = oneOf(['a', 'e', 'i', 'o', 'u'])
  .describe('Must be vowel');

# .then<V>(next: (blob: T, ok, err) => DecodeResult<V> | Decoder<V>): Decoder<V> (source)

# .then<V>(next: Decoder<V>): Decoder<V> (source)

Send the output of the current decoder into another decoder or acceptance function. The given acceptance function will receive the output of the current decoder as its input.

NOTE: This is an advanced, low-level, API. It’s not recommended to reach for this construct unless there is no other way. Most cases can be covered more elegantly by .transform(), .refine(), or .pipe() instead.


# .pipe<V>(next: Decoder<V>): Decoder<V> (source)

# .pipe<V>(next: (blob: T) => Decoder<V>): Decoder<V> (source)

const decoder =
  string
    .transform((s) => s.split(',').map(Number))
    .pipe(array(positiveInteger));

// πŸ‘
decoder.verify('7') === [7];
decoder.verify('1,2,3') === [1, 2, 3];

// πŸ‘Ž
decoder.verify('1,-3')  // -3 is not positive
decoder.verify('πŸš€');   // not a number
decoder.verify('3.14'); // not a whole number
decoder.verify(123);    // not a string
decoder.verify(true);   // not a string
decoder.verify(null);   // not a string

Dynamic decoder selection with .pipe()

With .pipe() you can also dynamically select another decoder, based on dynamic runtime value.

string
  .transform((s) => s.split(',').map(Number))
  .pipe((tup) =>
    tup.length === 2
      ? point2d
      : tup.length === 3
        ? point3d
        : never('Invalid coordinate'),
  );