nvie/decoders

Support for Enums

LcTwisk opened this issue · 7 comments

Let's say there is an enum like this:

enum Alphabet {
  a = "a",
  b = "b"
}

A possible decoder would be:

const decoder = either2(constant("a"), constant("b"))

This doesn't work for enums with more than 9 cases atm. What do you think of introducing another method that checks input agains an array of values with the same type?

Something like oneOf(values: [T])

nvie commented

I've only included "eithers" until either9, because you need to practically stop at a specific point. You can however just nest eithers if you need more than 9 cases. For example, if you need 13:

const either13 = either5(
  either9(
    ...  /* 9 cases here */
  ),
  ... /* 4 more cases */
)

Internally, either5 is also defined as either(either4(...), ...), so this isn't weird.

In your example of oneOf(values: [T]), what would T be? If you have oneOf(['foo', 'bar']) and want a decoder for 'foo' | 'bar', then I don't think this works, as the input type of ['foo', 'bar'] would be inferred as string[], not ('foo' | 'bar')[]. (So you would at best get a decoder for strings, not a union of string literals.) Can you elaborate a bit, or perhaps provide an example of how it would work?

PS: I think you probably want to use constant() rather than hardcoded() in your example?

Ahh I see!
Didn't think about nesting the either methods, this will work for my use case I guess 👍

For TS enums it would still be useful to add something like:

export function enum_(members: Array<string | number>): Decoder<string | number> {
    return (blob: mixed) => (members.includes(blob) ? Ok(blob) : Err(annotate(blob, `Must be one of ${members.join(', ')}`)));
}

PS: Yep, I meant constant indeed, will change it :)

nvie commented

For TS enums it would still be useful to add something like

I agree that this would be a nice generic decoder to add to the library, as a convenience for enforcing a string (or number) value is one of a few preset alternatives. It's good to note however that the output of such a decoder will always be a string at best, not a union of specific string constants.

If you have something like:

enum MyEnum {
  foo = "foo",
  bar = "bar",
}

const value = guard(oneOf(['foo', 'bar']))(request.body);

Then to the type system value will just be a string (even though at runtime this can only ever be 'foo' or 'bar'), but the type system doesn't know.

If you have a function that takes an Enum:

function doSomething(thing: MyEnum) { ... }

doSomething(value)
//          ^^^^^ TS2322: Type 'string' is not assignable to type 'MyEnum'

To solve this, you will still have to use either(constant('foo'), constant('bar')).

That said, if you want to use oneOf() as an alternative for something like regex() (check specific string values at runtime but return a string type), then yes I agree it's still a nice/useful addition.

@LcTwisk Ideas?

Hi there, any update on this?

nvie commented

Update: Since 2.3.0, support for enums comes out of the box, see the docs.

Old answer

There is no specific decoder for native enums at the moment. In decoders 2.0, the most pragmatic way to build one is to use the following construction:

enum MyEnum {
  foo = "foo",
  bar = "bar",
}

const myEnumDecoder = oneOf(['foo', 'bar']) as Decoder<MyEnum>;

It works! Thank you @nvie

nvie commented

In the latest v2.3.0, enum_ is now fully supported (see docs).