Skip to main content
Accepts values accepted by any of the given decoders.

Signature

function either<TDecoders extends readonly Decoder<unknown>[]>(
  ...decoders: TDecoders
): Decoder<DecoderType<TDecoders[number]>>

Type Inference

either() creates a union type from all provided decoders:
const decoder = either(string, number, boolean);
// Type: Decoder<string | number | boolean>

const result = decoder.verify('hello');
// result is string | number | boolean

Basic Usage

import { either, string, number } from 'decoders';

const decoder = either(string, number);

decoder.verify('hello');     // 'hello'
decoder.verify(42);          // 42
decoder.verify(true);        // DecodeError

How It Works

The decoders are tried on the input one by one, in the given order. The first one that accepts the input “wins”. If all decoders reject the input, the input gets rejected.
import { either, string, constant } from 'decoders';

const decoder = either(
  constant('foo'),
  constant('bar'),
  string,
);

decoder.verify('foo');       // 'foo' (matched by first decoder)
decoder.verify('bar');       // 'bar' (matched by second decoder)
decoder.verify('baz');       // 'baz' (matched by third decoder)

Error Messages

When all decoders fail, either() collects all error messages:
import { either, string, number, boolean } from 'decoders';

const decoder = either(string, number, boolean);

decoder.verify([]);
// DecodeError:
// Either:
// - Must be string
// - Must be number
// - Must be boolean

Union Type Inference

either() correctly infers union types:
import { either, constant, object, string, number } from 'decoders';

const catDecoder = object({
  type: constant('cat'),
  meow: string,
});

const dogDecoder = object({
  type: constant('dog'),
  bark: string,
});

const animalDecoder = either(catDecoder, dogDecoder);

type Animal = DecoderType<typeof animalDecoder>;
// { type: 'cat'; meow: string } | { type: 'dog'; bark: string }

const animal = animalDecoder.verify({ type: 'cat', meow: 'mew' });
if (animal.type === 'cat') {
  animal.meow; // TypeScript knows this is a string
}

Nested Either

The error messages for nested either() calls are flattened for readability:
import { either, constant } from 'decoders';

const decoder = either(
  constant('A'),
  either(
    constant('B'),
    either(
      constant('C'),
      constant('D'),
    ),
  ),
);

decoder.verify('X');
// DecodeError:
// Either:
// - Must be "A"
// - Must be "B"
// - Must be "C"
// - Must be "D"
//
// (not nested as "Either: - Either: - Either: ...")

Multiple Decoders

either() accepts any number of decoders:
import { either, string, number, boolean, null_, undefined_ } from 'decoders';

const decoder = either(string, number, boolean, null_, undefined_);
// Type: Decoder<string | number | boolean | null | undefined>

decoder.verify('hello');     // ✓
decoder.verify(42);          // ✓
decoder.verify(true);        // ✓
decoder.verify(null);        // ✓
decoder.verify(undefined);   // ✓
decoder.verify([]);          // ✗

Performance Note

For tagged unions (objects with a discriminator field), consider using taggedUnion() instead for better performance and error messages:
// Good for general unions
const decoder1 = either(stringDecoder, numberDecoder, booleanDecoder);

// Better for tagged unions
const decoder2 = taggedUnion('type', {
  cat: catDecoder,
  dog: dogDecoder,
});

Implementation

Source: ~/workspace/source/src/unions.ts:57-83
export function either<TDecoders extends readonly Decoder<unknown>[]>(
  ...decoders: TDecoders
): Decoder<DecoderType<TDecoders[number]>> {
  if (decoders.length === 0) {
    throw new Error('Pass at least one decoder to either()');
  }

  type T = DecoderType<TDecoders[number]>;
  return define<T>((blob, _, err) => {
    // Collect errors here along the way
    const errors: Annotation[] = [];

    for (const decoder of decoders) {
      const result = (decoder as Decoder<T>).decode(blob);
      if (result.ok) {
        return result;
      } else {
        errors.push(result.error);
      }
    }

    // Decoding all alternatives failed, return the combined error message
    const text =
      EITHER_PREFIX + errors.map((err) => nest(summarize(err).join('\n'))).join('\n');
    return err(text);
  });
}

Build docs developers (and LLMs) love