Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support request: strict intersections of interface and partial #106

Closed
gunzip opened this issue Dec 24, 2017 · 9 comments · Fixed by #107
Closed

support request: strict intersections of interface and partial #106

gunzip opened this issue Dec 24, 2017 · 9 comments · Fixed by #107
Labels

Comments

@gunzip
Copy link

gunzip commented Dec 24, 2017

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


@gcanti
Copy link
Owner

gcanti commented Dec 27, 2017

@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' }

@gcanti gcanti added the bug label Dec 27, 2017
@gunzip
Copy link
Author

gunzip commented Dec 27, 2017

Yes indeed I'm extracting the static types and I can live without partials.

@gcanti
Copy link
Owner

gcanti commented Dec 27, 2017

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

@gunzip
Copy link
Author

gunzip commented Dec 27, 2017

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

@gcanti
Copy link
Owner

gcanti commented Dec 28, 2017

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.

@gunzip
Copy link
Author

gunzip commented Feb 14, 2018

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

@gcanti
Copy link
Owner

gcanti commented Feb 14, 2018

@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
  )
}

@gunzip
Copy link
Author

gunzip commented Feb 14, 2018

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<A, B> with t.Type<B, A>
  • replaced all occurrences of t.validate(a, b) with b.decode(a)
  • replace calls to either.toOption() with fold(_ => none, r => some(r))

@gcanti
Copy link
Owner

gcanti commented Feb 14, 2018

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'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants