nvie/decoders

Extracting types from a Decoder<T>

girvo opened this issue · 5 comments

girvo commented

You likely already know this, but I felt pretty chuffed when I got it working, and figured others might benefit from it!

https://gist.github.com/girvo/b4207d4fc92f6b336813d1404309baab

This demonstrates how one can extract the T from a given Decoder<T> into a real type definition, for use elsewhere in a code-base. Prior to this, I was duplicating it: writing a type Example = ... then writing a const example: Guard<Example> = guard(object(... with the run-time versions duplicated.

This lets one define them once for run-time, and re-use them at type-time!

About the only "gripe" I have, is that I'd love to be able to make the type exact.

// @flow
type Thing = {|
  name: string
|}

But I don't know whether this is possible through inference yet?

Perhaps by using the $Exact<T> utility type this can be achieved: https://flow.org/en/docs/types/utilities/#toc-exact

nvie commented

Hi @girvo – it's so awesome to receive this contribution! This is a great idea, and which I would love to document for others, as it nicely lets you DRY up the definitions.

Re: the exactness of objects: the problem is that the object() decoder itself does not return an exact type. I've been thinking about adding an optional exact param to it, so you could call it like so:

const thing = guard(object({
  name: string,
}, /* exact */ true));

thing({ name: 'foo' })   // OK
thing({})  // error: missing key "name"
thing({ name: 'foo', age: 20 })  // error: superfluous key "age"

An alternative, more nicely composed syntax would be:

const thing = exact(object({
  name: string,
})

But I'm not sure if that will still enable nice error messaging.
Anyway, this is a cool idea!

girvo commented

Thanks! I'm glad you dig it; I've been fighting against duplication in type land and run-time forever now, and I'm stoked that decoders works so well for it!

I think that the second param would be nicer, even though it doesn't look quite as clean. Consider the nested object case:

const example = exact(object({
  another: object({ age: number })
}))

The "inner" object won't be exact (unless you do some really funky stuff with $ObjMap perhaps!), and having to write exact(...) over and over might become a bit annoying?

But, that's a silly quibble really!

Another feature from the utility types that would be great, is being able to define an object as $ReadOnly -- being able to ensure immutability at the type-level would be brilliant :)

nvie commented

I had a quick play with these ideas on a scratch pad this evening, and concluded the following:

  1. It turns out the object(..., /* exact */ true) variant isn't feasible. This would let the output type of the decoder be different based on the value passed in. This makes static typing impossible.
  2. I don't think we have to use exact(object(...)), but can simply offer exact(...), which would be similar to object(...), just output the $Exact<...> version of it. This is safe, since $Exact<T> only makes sense on object types anyway. I think the exact(...) decorator can be expressed in terms of the normal object(...) decoder, basically wrapping it and checking upfront for any superfluous keys, and force-type $Exact<...> on the result, a claim which will be guaranteed at run-time.
  3. Before adding exact(...), I'd to clean up error handling of the object(...) decoder first, before adding more logic to it. I have left a few TODO notes in there, and this might be a perfect time to address them and clean it up. (For example, I'd like to report all missing keys at once, rather than one by one.)
  4. I like the idea of using $ReadOnly<...> for all of these outputted objects by default — thanks for the idea.
nvie commented

About the only "gripe" I have, is that I'd love to be able to make the type exact.

I just pushed a change that adds support for using exact(), a drop-in for where you're using object() now, i.e.

exact({ name: string })

Which will give you a decoder for {| name: string |}.

@girvo Would you like to try it out? You can get it from this branch. Feedback would be very welcomed!

nvie commented

Support for exact() has shipped today, in 1.6.0. Thanks for the idea, @girvo!