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

Add withLazyFallback type #105

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

Conversation

lauritzsh
Copy link

I made a version of withFallback where you can supply a function that will be called. I am not sure whether its best to have just one function or two distinct. Saw it requested in #103 and could also use it in a project.

Copy link

@Frikki Frikki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me. It worries me that @gcanti hasn't made any comments on this PR or merged it, since it's been here since September 2019.

@gcanti
Copy link
Owner

gcanti commented Jan 27, 2020

@Frikki my fault, forgot to comment. @lauritzsh I'm sorry

We could put withLazyFallback in the same file of withFallback and deprecate withFallback

@lauritzsh
Copy link
Author

No worries, that sounds good. Will the withFallback file have two exports or will you replace the current implementation with the lazy version?

@gcanti
Copy link
Owner

gcanti commented Jan 27, 2020

@lauritzsh two exports for now (I'll replace the deprecated withFallback in the next breaking change release)

@Frikki
Copy link

Frikki commented Jan 27, 2020

All good, @gcanti. I hoped it was merely an oversight. 😄

@VanTanev
Copy link

VanTanev commented Jul 1, 2020

I was looking at doing something similar, but passing the whole object under validation, however it doesn't seem it's doable. Would have been nice to be able to do something like:

const UserCodec = t.type({
  firstName: t.string,
  lastName' t.string,
  fullName: withObjectFallback(t.string, (i: t.InputOf<typeof UserCodec>) => i?.firstName + ' ' + i?.lastName)
})

However, there doesn't seem to be a way for a the codec at fullName to see the object given to t.type()

I'm not sure what a good approach would be here. Maybe a more loose codec first, chained into the full user codec? But it starts getting quite unwieldy.

@gcanti If you find the time, would love to hear your thoughts.

@gcanti
Copy link
Owner

gcanti commented Jul 2, 2020

@VanTanev ideally decoders shouldn't have so much context, in order to be really composable they should be able to work in isolation, without depending on some specific context.

p.s.
^ this can be said for any functional programming abstraction

@VanTanev
Copy link

VanTanev commented Jul 2, 2020

@gcanti I guess the basic approach would be to have a more lax and a more strict codec, and chain them together. This is what I came up with, but it seems shaky at best.

I kind of want to have a codec be the ultimate interface exposed, instead my own ad-hoc function, as my API interface literally takes a URL and a codec as inputs.

const UserCodecBase = t.type({
    firstName: t.string,
    lastName: t.string,
    fullName: t.union([t.undefined, t.string]),
})
const UserCodecRefined = t.type({
    fullName: t.string,
})
const UserCodecCombined = t.type({
    ...UserCodecBase.props,
    ...UserCodecRefined.props,
})

export type User = t.TypeOf<typeof UserCodecCombined>

export const UserCodec = new t.Type(
    UserCodecCombined.name,
    (u): u is User => UserCodecCombined.is(u),
    (u, c) =>
        pipe(
            UserCodecBase.validate(u, c),
            E.map(user => {
                user.fullName = user.fullName ?? user.firstName + ' ' + user.lastName
                return user
            }),
            E.chain(user => UserCodecCombined.validate(user, c))
        ),
    UserCodecCombined.encode,
) as t.TypeC<typeof UserCodecCombined.props>

@mlegenhausen
Copy link
Contributor

@VanTanev can the fullName be different that firstName lastName? If not you should compute it via a function fullName(user: UserCodec): string. The problem with this approach is that I can create UserCodec instances where I can break this scheme.

@VanTanev
Copy link

VanTanev commented Jul 3, 2020

It can be different, but it's OK to fallback to a computed version when not present. The real use case I'm running into is more complex, but the idea is still that there is a property that can come with a distinct value, or a fallback should be computed from other properties on the object.

When you say you can make an instance that breaks the scheme, is that because I didn't use the t.InterfaceType constructor in the implementation above? That is easily fixable, but the code is quite clunky.

Ultimately, I'm looking for a way to build a codec that is validate: lax codec -> transform -> strict codec, encode: strict codec

@mlegenhausen
Copy link
Contributor

@VanTanev I think I would not try to encode your complex default value behavior in your codec. Just use io-ts for what it is intended for and this is validating data. Yes transformations are possible but as @gcanti mentioned only transformations that are possible without the surrounding context.

export const UserBase = t.type({
  firstName: t.string,
  lastName: t.string
})
export interface UserBase extends t.TypeOf<typeof UserBase> {}

export const UserWithMaybeFullName = t.intersection([UserBase, t.partial({ fullName: t.string })])

export interface UserWithFullName extends UserBase {
  fullName: string
}

export function toUserWithFullName(user: UserWithMaybeFullName): UserWithFullName {
  return { ...user, fullName: user.fullName && user.firstName + ' ' + user.lastName }
}

pipe(UserWithMaybeFullName.decode(input), E.map(toUserWithFullName))

@VanTanev
Copy link

VanTanev commented Jul 3, 2020

Yeah, I'm aware of that approach, but we have API consumers setup that directly take an io-ts codec and output something like E.Either<NetworkError | t.Errors, t.TypeOf<C>>. There are both read and write API interfaces like this, and for that reason I really wanted a way to build a codec that does this.

Ultimately, this isn't too far off from what io-ts-types/lib/fromNullable provides, conceptually. Computing a default value from other values is, I think, a fairly common use case.

@mlegenhausen
Copy link
Contributor

mlegenhausen commented Jul 3, 2020

You can also shorten your code a little bit with withValidate

withValidate(
  UserCodecBase,
  (u, c) => pipe(
    UserCodecBase.validate(u, c),
    E.map(user => ({
      ...user,
      fullName: user.fullName ?? user.firstName + ' ' + user.lastName
    }))
  )
).pipe(UserCodecCombined)

@VanTanev
Copy link

VanTanev commented Jul 3, 2020

This is a great approach, I completely forgot about t.Type.pipe(). Thank you for your time and help!

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.

5 participants