Io-ts: support request: strict intersections of interface and partial

Created on 24 Dec 2017  路  9Comments  路  Source: gcanti/io-ts

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)
);


bug

Most helpful comment

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'

All 9 comments

@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:

  • swapped t.Type with t.Type
  • replaced all occurrences of t.validate(a, b) with b.decode(a)
  • replace calls to either.toOption() with fold(_ => none, r => some(r))

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'
Was this page helpful?
0 / 5 - 0 ratings