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

Make optional fields more user friendly #542

Open
safareli opened this issue Dec 14, 2020 · 6 comments
Open

Make optional fields more user friendly #542

safareli opened this issue Dec 14, 2020 · 6 comments
Labels
experimental something related to the experimental features

Comments

@safareli
Copy link
Contributor

Other decoding/validation libraries have more user friendly optionality support like this:

D.type({
  foo: D.string,
  baz: D.optional(D.string)
})

Would be great to have something like this here too. As discussed with @gcanti there was some issues with the stable API #140 #266 but he doesn't remember if he even tried this with the experimental API.

@gcanti gcanti added the experimental something related to the experimental features label Dec 14, 2020
@aroman
Copy link

aroman commented Mar 11, 2021

Any update on this? Would be very helpful.

@ruizb
Copy link

ruizb commented Mar 13, 2021

The only "elegant" way I found recently is to use optionFromNullable from the io-ts-types library. However, it uses an Option<A> instead of A | undefined, so maybe it's not exactly what you are looking for...

import * as t from 'io-ts'
import { optionFromNullable } from 'io-ts-types'

const Foo = t.type({
  foo: t.string,
  baz: optionFromNullable(t.string)
})

type Foo = t.TypeOf<typeof Foo> // { foo: string, baz: O.Option<string> }

console.log(Foo.decode({ foo: 'foo' }))             // right({ foo: 'foo', baz: none })
console.log(Foo.decode({ foo: 'foo', baz: 'baz' })) // right({ foo: 'foo', baz: some('baz') })
Software Version
fp-ts 2.9.5
io-ts 2.2.16
io-ts-types 0.5.15

@safareli
Copy link
Contributor Author

safareli commented Jun 5, 2021

After being inspired by sparceType from io-ts-extra I've implemented similar thing for Decoder:

import * as D from "io-ts/Decoder";
import { pipe } from "fp-ts/lib/function";
import { partition } from "fp-ts/lib/Record";

type AnyDecoder = D.Decoder<unknown, unknown>;

type Props = { [K in string]: AnyDecoder & Partial<Optional> };

export interface Optional {
  optional: true;
}

const isOptional = <T>(val: T & Partial<Optional>): val is T & Optional => {
  return val.optional ?? false;
};

type OptionalKeys<Base> = {
  [Key in keyof Base]: Base[Key] extends Optional ? Key : never;
}[keyof Base];

type RequiredKeys<Base> = {
  [Key in keyof Base]: Base[Key] extends Optional ? never : Key;
}[keyof Base];

type Sparse<P> = {
  [K in RequiredKeys<P>]: D.TypeOf<P[K]>;
} &
  {
    [K in OptionalKeys<P>]?: D.TypeOf<P[K]>;
  };

/**
 * Marks decoder as an `Optional`, intended to be used with `D.sparse`.
 *
 * @see sparse
 */
export const optional = <D extends AnyDecoder>(decoder: D): D & Optional => {
  return Object.assign({}, decoder, { optional: true as const });
};

/**
 * Combines `D.struct` and `D.partial` in a nice way where instead of:
 * ```ts
 * const Person = pipe(
 *   D.struct({ name: D.string }),
 *   D.intersect(D.partial({ age: D.number }))
 * )
 * ```
 *
 * You can do:
 * ```ts
 * const Person = sparse({
 *   name: D.string,
 *   age: optional(D.number),
 * })
 * ```
 *
 * While having a great type inference:
 * ```ts
 * // const: Person: D.Decoder<unknown, {
 * //   name: string;
 * //   age?: number | undefined;
 * // }>
 * })
 * ```
 */
export const sparse = <P extends Props>(
  props: P
): D.Decoder<unknown, { [K in keyof Sparse<P>]: Sparse<P>[K] }> => {
  const partitioned = pipe(props, partition(isOptional));
  return pipe(
    D.struct(partitioned.left),
    D.intersect(D.partial(partitioned.right))
  ) as any;
};

Also, It has really nice type inference.

Let me know if this is desired and will make PR to add this /cc @gcanti


Downside is that when you hover over the sparse inferred type for P is not as nice as it would have been in case of struct:
Screen Shot 2021-06-06 at 12 20 42 AM

@thewilkybarkid
Copy link
Contributor

The only "elegant" way I found recently is to use optionFromNullable from the io-ts-types library. However, it uses an Option<A> instead of A | undefined, so maybe it's not exactly what you are looking for...

Worth noting that it encodes to A | null rather than A | undefined, which can be unexpected (it decodes from A | null | undefined).

@thewilkybarkid
Copy link
Contributor

I think I have a simplistic Option/undefined-based codec working:

const optionalD: <I, A>(or: d.Decoder<I, A>) => d.Decoder<undefined | I, O.Option<A>> = or =>
  ({ decode: i => i === undefined ? E.right(O.none) : pipe(i, or.decode, E.map(O.some)) })

const optionalE: <I, A>(or: e.Encoder<I, A>) => e.Encoder<undefined | I, O.Option<A>> = or =>
  ({ encode: flow(O.map(or.encode), O.toUndefined) })

const optionalC = <I, O, A>(codec: c.Codec<I, O, A>) => c.make(optionalD(codec), optionalE(codec))

@mjburghoffer
Copy link

I added a PR to address this: #654

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

No branches or pull requests

6 participants