Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve interactions between recursive and exact types. #390

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

GriffinSchneider
Copy link

Currently, you can't create an exact recursion type.

RecursiveType doesn't satisfy the HasProps type, even if the type it's wrapping does. This is easy to fix by adding a HasPropsRecursive type to HasProps and then extending getProps to return getProps(codec.type) for recursive codecs.

But now we have another problem: calling exact on a RecursiveType forces the lazy evaluation that makes RecursiveType work. This causes issues if you're trying to have the exact be part of the recursion. For example, this fails because the function runs before T is defined:

const T = t.exact(t.recursion('T', () => t.partial({ rec: T })))

I fixed this by having exact delay its evaluation of props until forced by someone calling validate or encode on the result. I added some tests with a few more examples of things that didn't work before this pullreq.

Also, I bumped the version to 2.1.0 since I needed an @since for the new types.

Add HasProps interfaces for recursive and exact types over types that have
props. Stop exact from immediately forcing evaluation of recursive types.
@gcanti
Copy link
Owner

gcanti commented Dec 3, 2019

Currently, you can't create an exact recursion type.

What about

it('should strip additional properties (recursive)', () => {
  interface P {
    rec?: P
  }
  const T: t.Type<P> = t.recursion('T', () => t.exact(t.partial({ rec: T })))
  assertSuccess(T.decode({ a: 1, rec: { b: 2, rec: {} } }), { rec: { rec: {} } })
})

it('should strip additional properties (mutually recursive)', () => {
  interface P1 {
    a?: P2
  }
  interface P2 {
    b?: P1
  }
  const T1: t.Type<P1> = t.recursion('T1', () => t.exact(t.partial({ a: T2 })))
  const T2: t.Type<P2> = t.recursion('T2', () => t.exact(t.partial({ b: T1 })))
  assertSuccess(T1.decode({ x: 1, a: { y: 2, b: { z: 3, a: {} } } }), { a: { b: { a: {} } } })
})

@GriffinSchneider
Copy link
Author

GriffinSchneider commented Dec 15, 2019

Sorry it took me so long to respond, this required some investigation on my part 😅. Putting the exact inside the recursion does work for the examples I came up with, but my actual use-case is more complicated.

I'm doing some code generation to create io-ts codecs from an OpenAPI spec. My template to generate a codec looks like this (types removed for readability):

const codec = t.recursion('', () => t.intersection([
  supertype,   
  t.type({ ... required properties ... }),
  t.partial({ ... optional properties ... }),
]));

where supertype is another codec generated with this template, if required.

This all works fine, but now I want my codecs to strip any unknown keys. It's clear that I need to use t.exact, but it seems like there are some issues (or at least I don't understand how it works) taking an intersection of exact types. For example, I'm not sure what's going on here:

const codec = t.intersection([t.exact(t.type({})), t.exact(t.partial({a: t.number}))])
codec.decode({ a: 1 }) // => { }
codec.decode({ a: 1, b: 2 }) // => { a: 1 }
// Adding b to the input stops it from stripping the a?

I think I ran into some other weird cases, but it's difficult to extract them into reasonable examples. Anyway, this convinced me that I should just avoid taking an intersection of exact types altogether.

To avoid taking an intersection of exact types, I need to generate both exact and inexact versions of each codec - exact for my actual usage, and inexact for when another codec needs to intersect with this one. That leaves me with a template like this:

const inexactCodec = t.recursion('', () => t.intersection([
  inexactSupertype,   
  t.type({ ... required properties ... }),
  t.partial({ ... optional properties ... }),
]));
const exactCodec = t.recursion('', () => t.exact(t.intersection([
  inexactSupertype,   
  t.type({ ... required properties ... }),
  t.partial({ ... optional properties ... }),
])));

Which has some code duplication that I can't figure out a way to resolve, since it's stuck inside the recursion. With the changes in this pull request, I'm able to move the exact outside the recursion and resolve the duplication like this:

const inexactCodec = t.recursion('', () => t.intersection([
  inexactSupertype,   
  t.type({ ... required properties ... }),
  t.partial({ ... optional properties ... }),
]));
const exactCodec = t.exact(inexactCodec)

So, that's why I made the changes here. Now that I've written this up, I think that if I had figured out / fixed the interactions between intersection and exact maybe I could have kept the exact inside the recursion somehow.

I think this pullreq does enable something that there wasn't a way to do before, although the unit tests I added were already possible to do by moving the exact inside the recursion.

@gcanti
Copy link
Owner

gcanti commented Dec 15, 2019

@GriffinSchneider this looks like a bug

const codec = t.intersection([t.exact(t.type({})), t.exact(t.partial({a: t.number}))])
codec.decode({ a: 1 }) // => { }
codec.decode({ a: 1, b: 2 }) // => { a: 1 }
// Adding b to the input stops it from stripping the a?

I'll open an issue

@gcanti
Copy link
Owner

gcanti commented Dec 16, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants