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.
@mindjuice https://github.com/pelotom/runtypes/issues/15#issuecomment-307601297
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
Most helpful comment
Hi folks, there still isn't a good way to specify optional, right?