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

Option fields: intersection + partial vs union with undefined #415

Closed
hjr3 opened this issue Jan 21, 2020 · 4 comments
Closed

Option fields: intersection + partial vs union with undefined #415

hjr3 opened this issue Jan 21, 2020 · 4 comments

Comments

@hjr3
Copy link

hjr3 commented Jan 21, 2020

📖 Documentation

I am familiar with Mixing required and optional props and define my objects like:

const V = t.intersection([
   t.type({
      foo: t.string,
    }),
    t.partial({
       bar: t.string,
    }),
]);

However, I had a colleague define it like:

const V = t.type({
   foo: t.string,
   bar: t.union([t.string, t.undefined]),
});

Are these equivalent or is one preferred over the other?

@gcanti
Copy link
Owner

gcanti commented Jan 22, 2020

Are these equivalent

Theoretically no

import * as t from 'io-ts'

const V1 = t.intersection([
  t.type({
    foo: t.string
  }),
  t.partial({
    bar: t.string
  })
])

const V2 = t.type({
  foo: t.string,
  bar: t.union([t.string, t.undefined])
})

type V1 = t.TypeOf<typeof V1>
/*
type V1 = {
  foo: string;
  bar?: string | undefined;
}
*/
type V2 = t.TypeOf<typeof V2>
/*
type V2 = {
    foo: string;
    bar: string | undefined;
}
*/

^ the bar field is optional in V1 (see ? in: bar?: string | undefined)
while required in V2 (no ? in: bar: string | undefined)

In practice yes they are equivalent, if you use V1 or V2 as decoders.

V2 is just a little bit more annoying if you use it as encoder

V1.encode({ foo: 'foo' }) // you can omit `bar`
V2.encode({ foo: 'foo', bar: undefined })

@lostintime
Copy link
Collaborator

You could do some type magic to fix the required key issue, ex:

/**
 * Type lambda returning a union of field names from input type A having type P
 * Inspired by https://stackoverflow.com/questions/55247766/check-if-an-interface-has-a-required-field
 */
type FieldsWith<A, P> = { [K in keyof P]-?: (A extends P[K] ? K : never) }[keyof P]

/**
 * Dual for FieldsWith - returns the rest of the fields
 */
type FieldsWithout<A, P> = Exclude<keyof P, FieldsWith<A, P>>

/**
 * Typa lambda returning new type with all fields within P having type U marked as optional
 */
type MakeOptional<P, U = undefined> = Pick<P, FieldsWithout<U, P>> & Partial<Pick<P, FieldsWith<U, P>>>

/**
 * Fix signature by marking all fields with undefined as optional
 */
const fixOptionals = <C extends t.Mixed>(c: C): t.Type<MakeOptional<t.TypeOf<C>>, t.OutputOf<C>, t.InputOf<C>> => c

/**
 * Just an alias for T | undefined codec
 */
const optional = <C extends t.Mixed>(c: C): t.Type<t.TypeOf<C> | undefined, t.OutputOf<C>, t.InputOf<C>> =>
  t.union([t.undefined, c])

/**
 * Example type
 */
const Profile = fixOptionals(t.type({
  name: t.string,
  age: optional(t.number)
}))

type Profile = t.TypeOf<typeof Profile>

/**
 * the type can now be initialized ignoring undefined fields
 */
const someProfile: Profile = { name: "John" }

Profile.encode(someProfile) // Ok

Profile.encode({ }) // Not ok

console.log(Profile.decode(someProfile))

// prints:
// 
// right({
//   "name": "John"
// })

This topic was pretty often discussed within several issues, maybe we can add some support to the core library, the optional field syntax seems prettier for me too, but the solution above also adds some more verbosity to type hints.

@hjr3
Copy link
Author

hjr3 commented Jan 22, 2020

Thank you both for the thoughtful response.

@hjr3 hjr3 closed this as completed Jan 22, 2020
@mmkal
Copy link
Contributor

mmkal commented Jan 22, 2020

See #266

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

No branches or pull requests

4 participants