Io-ts: Simpler optional props syntax?

Created on 6 Jul 2017  路  12Comments  路  Source: gcanti/io-ts

I'm using io-ts in a current project and its working quite well, but there is one thing I'm not so keen on.

The current mechanism for creating an interface with some optional props is rather verbose.

const A = t.interface({
  foo: t.string
})

const B = t.partial({
  bar: t.number
})

const C = t.intersection([A, B])

type CT = t.TypeOf<typeof C>

Would it be possible to have something like the following instead, even if, under the hood, it ended up creating the more complex version?

const A = t.interface({
  foo: t.string,
  bar: t.optional(t.number)
})

type CT = t.TypeOf<typeof C>

I haven't yet looked at the code, but I would think you could iterate over the props and collect the ones set to optional and create a t.interface() and a t.partial() and combine them with t.intersection() as above.

Most helpful comment

Hi folks, there still isn't a good way to specify optional, right?

All 12 comments

Sorry, I'm not sure what bit of that conversation you want to call out. It sounds like the same basic question on a different project. Are you referring to the part where they say it's not possible in TypeScript?

I'm not asking to do anything different in the end than what the long-winded version already does, so it seems possible from a types point of view.

Or am I missing something in the referenced thread?

@mindjuice indeed, not possible.
To understand why, just have a look at how it's implemented.

@mindjuice the problem is that making a prop optional at the type level means to use a mapped type and the ? modifier on the keys

export type PartialOf<P extends Props> = { [K in keyof P]?: TypeOf<P[K]> }

so I always need at least two mapped types, one for the required props and one for the optional props.

Some boilerplate can be avoided by defining a helper though

export function mixed<R extends t.Props, O extends t.Props>(
  required: R,
  optional: O,
  name?: string
): t.IntersectionType<[t.InterfaceType<R>, t.PartialType<O>], t.InterfaceOf<R> & t.PartialOf<O>> {
  return t.intersection([t.interface(required), t.partial(optional)], name)
}

const C = mixed(
  {
    foo: t.string
  },
  {
    bar: t.number
  }
)

type Clean<T> = Pick<T, keyof T>

type CT = Clean<t.TypeOf<typeof C>>
/*
type CT = {
    foo: string;
    bar?: number | undefined;
}
*/

It's not what you are asking for but I can't think of any other decent solution at the moment.

Thank you for the explanation and the code snippet. Making more sense now.

Last night I tried writing a quick and dirty version of what I described, which I've pasted below, and although validate() works and the unit tests pass, I get no type checking in Visual Studio code from the type definition, presumably because it's no longer statically analyzable.

import * as t from 'io-ts'
import { ThrowReporter } from 'io-ts/lib/reporters/default'

class Optional {
  tNode: any

  constructor(tNode: any) {
    this.tNode = tNode
  }
}

function optional(tNode: any) {
  return new Optional(tNode)
}

function makeInterface(obj: Object) {
  const required = {}
  const optional = {}

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const item: any = obj[key]
      if (item instanceof Optional) {
        optional[key] = item.tNode
      } else {
        required[key] = item
      }
    }
  }

  return t.intersection([t.interface(required), t.partial(optional)])
}

export const TestIntf = makeInterface({
  foo: t.number,
  bar: optional(t.string),
  baz: t.string,
})
export type TestIntf = t.TypeOf<typeof TestIntf>

describe('io-ts', () => {
  test('something', () => {
    // These should not throw errors
    expect(() => ThrowReporter.report(t.validate({ foo: 123, bar: 'abc', baz: 'xyz' }, TestIntf))).not.toThrow()
    expect(() => ThrowReporter.report(t.validate({ foo: 123, baz: 'xyz' }, TestIntf))).not.toThrow()

    // These should throw errors
    expect(() => ThrowReporter.report(t.validate({ foo: 'abc', baz: 'xyz' }, TestIntf))).toThrow()
    expect(() => ThrowReporter.report(t.validate({ foo: 123, baz: 123 }, TestIntf))).toThrow()
    expect(() => ThrowReporter.report(t.validate({ foo: 'abc', baz: 'xyz' }, TestIntf))).toThrow()
    expect(() => ThrowReporter.report(t.validate({ foo: 'abc' }, TestIntf))).toThrow()
    expect(() => ThrowReporter.report(t.validate({}, TestIntf))).toThrow()
  })
})

@mindjuice yes indeed, at the value level you can do almost whatever you want, the problem resides at the type level where we are constrained by the available features (mapped types are a "all or none" feature: either all fields are required or all fields are optional)

the problem is that making a prop optional at the type level means to use a mapped type and the ? modifier on the keys

@gcanti Why does it need ? on the keys? If we're reading in data at runtime, can't we just have an OptionalType<...> and then the validation for OptionalType<> just allows undefined?

@jez9999 If you are not interested in the ? in the keys you can just use a required field and a union

const Person = t.interface({
  name: t.string,
  age: t.union([t.number, t.undefined])
})

console.log(t.validate({ name: 'Giulio' }, Person).fold(() => 'error', p => 'ok')) // ok

the inferred type will be misleading though

type Person = t.TypeOf<typeof Person>
/* 
type Person = {
    name: string;
    age: number | undefined;
}

instead of

type Person = {
    name: string;
    age?: number;
}
*/

console.log(t.validate({ name: 'Giulio' }, Person).fold(() => 'error', p => String(p.hasOwnProperty('age')))) // false

@gcanti Is there a way I can alias that undefined union to a type called "Partial" or something?

@jez9999 yes you can define your own combinators https://github.com/gcanti/io-ts#the-maybe-combinator

Hi folks, there still isn't a good way to specify optional, right?

@ilyakatz I don't think so

Was this page helpful?
0 / 5 - 0 ratings

Related issues

leemhenson picture leemhenson  路  6Comments

mottetm picture mottetm  路  5Comments

gabro picture gabro  路  4Comments

steida picture steida  路  3Comments

xogeny picture xogeny  路  5Comments