mojotech / json-type-validation

TypeScript JSON type validation
MIT License
155 stars 14 forks source link

Enum decoders are cumbersome and not intuitive #18

Open mulias opened 6 years ago

mulias commented 6 years ago

Given a TS enum

enum Directions { North, South, East, West }

We currently write the decoder as

const directionsDecoder: Decoder<Directions> = oneOf(
  constant(Directions.North), 
  constant(Directions.South), 
  constant(Directions.East), 
  constant(Directions.West)
);

which isn't great.

It's worth noting that the most ideal syntax is not possible, since TS compiles out enums and hardcodes the enum values during compilation

const directionsDecoder = enumDecoder(Directions); // this doesn't work!

Something like this should be possible:

const enumDecoder = <E>(...enumValues: Array<E[keyof E]>): Decoder<E> =>
  oneOf(...enumValues.map((v) => constant(v)));

const directionsDecoder = enumDecoder(
  Directions.North, Directions.South, Directions.East, Directions.West
);

But no matter how I bash at the types I am unable to get the type checker to reconcile the relationship between the elements of the enum and the enum in total.

RocketPuppy commented 6 years ago

@mulias This says that "enums are real objects at runtime" https://www.typescriptlang.org/docs/handbook/enums.html, so I would expect enumDecoder(Directions) to work. Is that a recent change?

mulias commented 6 years ago

@RocketPuppy I had looked into this a while ago and had forgotten some of the details. The issue I found is that if you define an enum then it has an object at runtime which can be used to create a decoder, but if you define a const enum it is removed during compilation. My concern was that it seemed like there was no static warning about using a const enum as if it existed at runtime, and I didn't want to have special documentation that said "this helper will only work for non-const enums and will otherwise fail at runtime without warning." Maybe I need to re-investigate that option.

I should note that shortly after posting this issue I realized that it's totally possible to write the enum decoder inside of the Decoder class defined in this library, but it can't be done as a helper function like I was attempting above.

RocketPuppy commented 6 years ago

Ah, I thought it might have been an issue with const enums. You could have two decoders. One that takes an enum object and one that takes enum members. I think Typescript should prevent you from using the const enum with the enum object decoder.

mulias commented 6 years ago

Only one way to find out :)

mulias commented 6 years ago

New issue with enumDecoder(Directions)

Object.keys(Directions)
// [  '0', '1', '2', '3', 'North', 'South', 'East', 'West' ]

Enum runtime objects are implemented as two way mappings, so if all I have is the enum then I can't actually tell which values are the keys.

All the ways around that seem pretty hacky, so I think we're stuck with listing out the enum values.

katanacrimson commented 5 years ago

It appears that the behavior is heavily intended, and is also specific to numeric values for enums.

If the enum uses string values, no reverse mapping happens - so at least for string enums, it becomes possible to make the assumption that you can coerce the enum into an array and use an Array.includes() call to verify if the enum is a member. However, there's no easy way to verify if the enum is a string enum or not, making it something that can only be done by hand.

More info here: https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings Also related: https://github.com/Microsoft/TypeScript/issues/30487