Extracting types from a Decoder<T>
girvo opened this issue · 5 comments
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
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!
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 :)
I had a quick play with these ideas on a scratch pad this evening, and concluded the following:
- 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. - I don't think we have to use
exact(object(...))
, but can simply offerexact(...)
, which would be similar toobject(...)
, just output the$Exact<...>
version of it. This is safe, since$Exact<T>
only makes sense on object types anyway. I think theexact(...)
decorator can be expressed in terms of the normalobject(...)
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. - Before adding
exact(...)
, I'd to clean up error handling of theobject(...)
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.) - I like the idea of using
$ReadOnly<...>
for all of these outputted objects by default — thanks for the idea.
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!