Open safareli opened 3 years ago
Any update on this? Would be very helpful.
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 |
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
:
The only "elegant" way I found recently is to use
optionFromNullable
from the io-ts-types library. However, it uses anOption<A>
instead ofA | 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
).
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))
I added a PR to address this: https://github.com/gcanti/io-ts/pull/654
Other decoding/validation libraries have more user friendly optionality support like this:
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.