io-ts icon indicating copy to clipboard operation
io-ts copied to clipboard

Add codec for when multiple attributes are required in unison

Open Liam-Tait opened this issue 3 years ago • 0 comments

🚀 Feature request

Current Behavior

Often I come across a situation where if I have one attribute, another attribute should also be required For example: A by itself is ok, but if B is defined C also needs to be defined and vice versa

{ A: ""} // Ok
{ A: "", B: "", C: "" } // Ok
{ A: "", B: undefined, C: undefined } // Ok
{ A: "", B: undefined } // Ok
{ A: "", C: undefined } // Ok
{ A: "", B: "" } // Not Ok
{ A: "", C: "" } // Not Ok

To make this work right now I create a each type individually like the following

const myCodec = t.intersection([
    t.type({ A: t.string }),
    t.union([
        t.type({ B: t.string, C: t.string }),
        t.partial({ B: t.undefined, C: t.undefined }),
    ]),
])

Desired Behavior

Adding a convenience function that:

  • If one property is defined, all properties match their type
  • If one property is undefined, all properties are undefined or not included
  • if one property is not included, all properties are undefined or not included

Suggested Solution

const myCodec = t.intersection([
    t.type({ A: t.string }),
    t.optionalType({ B: t.string, C: t.string })
])

Unsure of naming:

  • t.allOrNone
  • t..optional
  • t.optionalType
  • t.together

Who does this impact? Who is this for?

In my particular case this is fairly common pattern for request body validation on and api. My guess is that this is a common scenario

Describe alternatives you've considered

Writing the convenience function outside of the library

A loose untested version

const optionalType = <P extends t.Props>(
    props: P
) => {
    const toUndefinedEntry = (key: string) => [key, t.undefined] as const
    const undefinedEntries = Object.keys(props).map(toUndefinedEntry)
    const obj = Object.fromEntries(undefinedEntries) as Record<
        keyof P,
        t.UndefinedC
    >
    return t.union([t.type(props), t.partial(obj)])
}

const myCodec2 = t.intersection([
    t.type({ A: t.string }),
    optionalType({ B: t.string, C: t.string }),
])

Using the current solution of duplicating the pattern for each usage

const myCodec = t.intersection([
    t.type({ A: t.string }),
    t.union([
        t.type({ B: t.string, C: t.string }),
        t.partial({ B: t.undefined, C: t.undefined }),
    ]),
])

Additional context

I tried to make the convince type by using t.record with t.partial

const optionalType = <P extends t.Props>(props: P) =>
    t.union([
        t.type(props),
        t.partial(t.record(t.keyof(props), t.union))
    ])

However t.partial and t.record is not compatible, it looks like https://github.com/gcanti/io-ts/issues/429 is related this

Your environment

Software Version(s)
io-ts 2.2.16
fp-ts 2.11.4
TypeScript 4.4.3

Liam-Tait avatar Oct 22 '21 00:10 Liam-Tait