#617·io-ts

nullable and optional combinators in stable module

Author: etiennebatiseCreated Nov 5, 2021Updated Jan 18, 2024

Feature request

Current Behavior

To define optional or nullable fields using the stable api (residing in the index module), we have to define unions :

typescript
const Person = t.type({
  name: t.string,
  nullable: t.union([t.null, t.number]),
  optional: t.union([t.undefined, t.string])
})

An annoying consequence of that is described in this issue. tldr: when reporting, we get an error for each member of the union More on why this is annoying, at least to me, later.

Desired Behavior

It'd be nice to have some combinators to make those type definitions much more readable.

typescript
const Person = t.type({
  name: t.string,
  nullable: t.nullable(number),
  optional: t.optional(t.string)
})

And it'd be nice to get around the issue of distinct errors for the same field.

A nullable combinator is already available in the Decoder module.

Suggested Solution

I was not able to come up for a solution by myself so I just took the author's code from the issue mentioned above. Note that I had to add a smelly as undefined to make this typecheck on my local setup.

typescript
function optional<A, O>(
  codec: t.Type<A, O, unknown>,
  name: string = `${codec.name} | undefined`
): t.Type<A | undefined, O | undefined, unknown> {
  return new t.Type(
    name,
    (u: unknown): u is A | undefined => u === undefined || codec.is(u),
    (u, c) => (u === undefined ? t.success(u) : codec.validate(u, c)),
    a => (a === undefined ? a as undefined : codec.encode(a))
  ) //                        ^ Smelly code right here
}

Similar thing for nullable. I monkey patched the above with no shame:

typescript
function nullable<A, O>(
  codec: t.Type<A, O, unknown>,
  name: string = `${codec.name} | null`
): t.Type<A | null, O | null, unknown> {
  return new t.Type(
    name,
    (u: unknown): u is A | null => u === null || codec.is(u),
    (u, c) => (u === null ? t.success(u) : codec.validate(u, c)),
    a => (a === null ? a as null : codec.encode(a))
  ) //                   ^ Smelly code right here again
}

Who does this impact? Who is this for?

This is for all users.

Describe alternatives you've considered

We could also provide those snippets as part of the docs or some other lib (eg: io-ts-types)

Additional context

My initial goal was to create a JSON path reporter for my types. Unfortunately, when crawling the error contexts, I could not understand how to efficiently distinguish union context errors from array items errors. For instance, given the following type

typescript
export const Person = t.type({
  books: t.union([t.null, t.array(
    t.type({
        title: t.string
    })
  )])
})
const foo = Person.decode({ books:[{title: "Foo"}, {}]})

My custom error reporter would give me the following:

typescript
[
  '.books.[0]',
  '.books.[1].[1].title'
 ]

There is obviously no array of arrays in the original type but the only hint I get in the error context is if the key is a number (with isNan(parseInt)). In that case, I wrap the key between brackets. Also, this won't even work for dictionaries with number fields.

Maybe I am missing something completely obvious and written in bold in the docs and all this is useless.

Your environment

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