Is it possible to have a "strict" intersection of optional and mandatory properties ?
I've tried the following without success:
import * as t from "io-ts";
import { failure } from "io-ts/lib/PathReporter";
const required = t.interface({
foo: t.string
});
const optional = t.partial({
bar: t.string
});
const all = t.intersection([
t.strict(required.props),
t.strict(optional.props)
]);
console.log(
t
.validate(
{
foo: "foo",
bar: "bar"
},
all
)
.fold(errors => {
throw new Error(failure(errors).join("\n"));
}, t.identity)
);
//////// errors
or:
import * as t from "io-ts";
import { failure } from "io-ts/lib/PathReporter";
const required = t.interface(
t.strict({
foo: t.string
}).props
);
const optional = t.partial(
t.strict({
bar: t.string
}).props
);
const all = t.intersection([required, optional]);
console.log(
t
.validate(
{
foo: "foo",
bar: "bar",
x: 1 //////// <----------------- validates
},
all
)
.fold(errors => {
throw new Error(failure(errors).join("\n"));
}, t.identity)
);
@gunzip are you extracting the static type from all?
If you are not extracting the static type you should be able to avoid using partial
const T = t.strict({
foo: t.string,
bar: t.union([t.string, t.undefined])
})
However currently this is failing
console.log(
t.validate({ foo: 'foo' }, T).fold(errors => {
throw new Error(failure(errors).join('\n'))
}, t.identity)
)
// Invalid value {"foo":"foo"} supplied to : StrictType<{ foo: string, bar: (string | undefined) }>
which means that strict is not aligned to interface
const T = t.interface({
foo: t.string,
bar: t.union([t.string, t.undefined])
})
console.log(
t.validate({ foo: 'foo' }, T).fold(errors => {
throw new Error(failure(errors).join('\n'))
}, t.identity)
)
// { foo: 'foo' }
Yes indeed I'm extracting the static types and I can live without partials.
Then it's not possible with the available combinators, you could define your own though, something along the lines of
function strictInterfaceWithOptionals<R extends t.Props, O extends t.Props>(
required: R,
optional: O,
name?: string
): t.Type<any, t.InterfaceOf<R> & t.PartialOf<O>> {
const loose = t.intersection([t.interface(required), t.partial(optional)])
const props = Object.assign({}, required, optional)
return new t.Type(
name || `StrictInterfaceWithOptionals(${loose.name})`,
loose.is,
(s, c) =>
loose.validate(s, c).chain(o => {
const keys = Object.getOwnPropertyNames(o)
const len = keys.length
const errors: t.Errors = []
for (let i = 0; i < len; i++) {
const key = keys[i]
if (!props.hasOwnProperty(key)) {
errors.push(t.getValidationError(o[key], t.appendContext(c, key, t.never)))
}
}
return errors.length ? t.failures(errors) : t.success(o)
}),
loose.serialize
)
}
const T = strictInterfaceWithOptionals(
{
foo: t.string
},
{
bar: t.union([t.string, t.undefined])
}
)
console.log(
t.validate({ foo: 'foo', a: 1 }, T).fold(errors => {
throw new Error(failure(errors).join('\n'))
}, t.identity)
)
// Invalid value 1 supplied to : StrictMixedInterface(({ foo: string } & PartialType<{ bar: (string | undefined) }>))/a: never
thank you this works pretty well ! maybe it's a common case that worths a mention in the docs at least ?
export function strictInterfaceWithOptionals<
R extends t.Props,
O extends t.Props
>(
required: R,
optional: O,
name: string
): t.Type<{}, t.InterfaceOf<R> & t.PartialOf<O>> {
const loose = t.intersection([t.interface(required), t.partial(optional)]);
const props = Object.assign({}, required, optional);
return new t.Type(
name,
loose.is,
(s, c) =>
loose.validate(s, c).chain(o => {
const errors: t.Errors = Object.getOwnPropertyNames(o)
.map(
key =>
!props.hasOwnProperty(key)
? t.getValidationError(o[key], t.appendContext(c, key, t.never))
: undefined
)
.filter((e): e is t.ValidationError => e !== undefined);
return errors.length ? t.failures(errors) : t.success(o);
}),
loose.serialize
);
}
Note: in my implementation is is wrong, should be
(v): v is t.InterfaceOf<R> & t.PartialOf<O> =>
loose.is(v) && Object.getOwnPropertyNames(v).every(k => props.hasOwnProperty(k))
maybe it's a common case that worths a mention in the docs at least ?
Sure, maybe in the "Recipe" section (https://github.com/gcanti/io-ts#recipes). I'll gladly accept a PR.
hi @gcanti , may you suggest a way to refactor this with the latest 1.x version ?
I've tried with the following without success:
export function strictInterfaceWithOptionals<
R extends t.Props,
O extends t.Props
>(
required: R,
optional: O,
name: string
): t.Type<t.InterfaceType<R> & t.PartialType<O>, t.mixed> {
const loose = t.intersection([t.interface(required), t.partial(optional)]);
const props = Object.assign({}, required, optional);
return new t.Type(
name,
(v): v is t.InterfaceType<R> & t.PartialType<O> =>
loose.is(v) &&
Object.getOwnPropertyNames(v).every(k => props.hasOwnProperty(k)),
// tslint:disable-next-line:readonly-array
(s: t.mixed, c: t.ContextEntry[]) =>
loose.validate(s, c).chain(o => {
const errors: t.Errors = Object.getOwnPropertyNames(o)
.map(
key =>
!props.hasOwnProperty(key)
? t.getValidationError(o[key], t.appendContext(c, key, t.never))
: undefined
)
.filter((e): e is t.ValidationError => e !== undefined);
return errors.length ? t.failures(errors) : t.success(o);
}),
loose.encode
);
}
@gunzip try this
export function strictInterfaceWithOptionals<R extends t.Props, O extends t.Props>(
required: R,
optional: O,
name?: string
): t.Type<
{ [K in keyof R]: t.TypeOf<R[K]> } & { [K in keyof O]?: t.TypeOf<O[K]> },
{ [K in keyof R]: t.OutputOf<R[K]> } & { [K in keyof O]?: t.OutputOf<O[K]> }
> {
const loose = t.intersection([t.interface(required), t.partial(optional)])
const props = Object.assign({}, required, optional)
return new t.Type(
name || `StrictInterfaceWithOptionals(${loose.name})`,
(m): m is t.TypeOfProps<R> & t.TypeOfPartialProps<O> =>
loose.is(m) && Object.getOwnPropertyNames(m).every(k => props.hasOwnProperty(k)),
(m, c) =>
loose.validate(m, c).chain(o => {
const errors: t.Errors = Object.getOwnPropertyNames(o)
.map(
key =>
!props.hasOwnProperty(key) ? t.getValidationError(o[key], t.appendContext(c, key, t.never)) : undefined
)
.filter((e): e is t.ValidationError => e !== undefined)
return errors.length ? t.failures(errors) : t.success(o)
}),
loose.encode
)
}
It looks like it's working thank you ! (I'm in the middle of the big refactor to 1.x =)
By now I've:
replace calls to either.toOption() with fold(_ => none, r => some(r))
You can use fromEither from the Option module for that
import { fromEither } from 'fp-ts/lib/Option'
Most helpful comment
You can use
fromEitherfrom theOptionmodule for that