From 4feae9cd93707a9f4ae42ab868c211957bc89f9b Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Fri, 1 Apr 2022 09:34:05 +0100 Subject: [PATCH] Treat struct properties that have type undefined as optional --- docs/modules/Codec.ts.md | 8 +++++-- docs/modules/Decoder.ts.md | 48 ++++++++++++++++++++++++++++++++++---- docs/modules/Encoder.ts.md | 2 +- dtslint/ts3.5/Codec.ts | 24 ++++++++++++++++--- dtslint/ts3.5/Decoder.ts | 14 +++++++++-- dtslint/ts3.5/Encoder.ts | 5 +++- src/Codec.ts | 17 +++++++++++--- src/Decoder.ts | 18 ++++++++++---- src/Encoder.ts | 11 +++++++-- 9 files changed, 124 insertions(+), 23 deletions(-) diff --git a/docs/modules/Codec.ts.md b/docs/modules/Codec.ts.md index 0f5feca2..1a7c6388 100644 --- a/docs/modules/Codec.ts.md +++ b/docs/modules/Codec.ts.md @@ -147,7 +147,11 @@ Added in v2.2.3 ```ts export declare function fromStruct

>>( properties: P -): Codec<{ [K in keyof P]: InputOf }, { [K in keyof P]: OutputOf }, { [K in keyof P]: TypeOf }> +): Codec< + ToOptional<{ [K in keyof P]: InputOf }>, + ToOptional<{ [K in keyof P]: OutputOf }>, + ToOptional<{ [K in keyof P]: TypeOf }> +> ``` Added in v2.2.15 @@ -280,7 +284,7 @@ Added in v2.2.3 ```ts export declare function struct

>>( properties: P -): Codec }, { [K in keyof P]: TypeOf }> +): Codec }>, ToOptional<{ [K in keyof P]: TypeOf }>> ``` Added in v2.2.15 diff --git a/docs/modules/Decoder.ts.md b/docs/modules/Decoder.ts.md index 347edd76..96db1cf1 100644 --- a/docs/modules/Decoder.ts.md +++ b/docs/modules/Decoder.ts.md @@ -232,7 +232,21 @@ Added in v2.2.8 ```ts export declare const fromStruct:

>>( properties: P -) => Decoder<{ [K in keyof P]: K.InputOf<'Either', P[K]> }, { [K in keyof P]: K.TypeOf<'Either', P[K]> }> +) => Decoder< + { [K in keyof P]: K.InputOf<'Either', P[K]> }, + Merge< + Pick< + { [K in keyof P]: K.TypeOf<'Either', P[K]> }, + Exclude }>> + > & + Partial< + Pick< + { [K in keyof P]: K.TypeOf<'Either', P[K]> }, + UndefinedProperties<{ [K in keyof P]: K.TypeOf<'Either', P[K]> }> + > + > + > +> ``` Added in v2.2.15 @@ -371,7 +385,13 @@ Added in v2.2.7 ```ts export declare const struct: ( properties: { [K in keyof A]: Decoder } -) => Decoder +) => Decoder< + unknown, + Merge< + Pick<{ [K in keyof A]: A[K] }, Exclude>> & + Partial>> + > +> ``` Added in v2.2.15 @@ -433,7 +453,21 @@ Use `fromStruct` instead. ```ts export declare const fromType:

>>( properties: P -) => Decoder<{ [K in keyof P]: K.InputOf<'Either', P[K]> }, { [K in keyof P]: K.TypeOf<'Either', P[K]> }> +) => Decoder< + { [K in keyof P]: K.InputOf<'Either', P[K]> }, + Merge< + Pick< + { [K in keyof P]: K.TypeOf<'Either', P[K]> }, + Exclude }>> + > & + Partial< + Pick< + { [K in keyof P]: K.TypeOf<'Either', P[K]> }, + UndefinedProperties<{ [K in keyof P]: K.TypeOf<'Either', P[K]> }> + > + > + > +> ``` Added in v2.2.8 @@ -447,7 +481,13 @@ Use `struct` instead. ```ts export declare const type: ( properties: { [K in keyof A]: Decoder } -) => Decoder +) => Decoder< + unknown, + Merge< + Pick<{ [K in keyof A]: A[K] }, Exclude>> & + Partial>> + > +> ``` Added in v2.2.7 diff --git a/docs/modules/Encoder.ts.md b/docs/modules/Encoder.ts.md index 153decb6..8208a586 100644 --- a/docs/modules/Encoder.ts.md +++ b/docs/modules/Encoder.ts.md @@ -167,7 +167,7 @@ Added in v2.2.3 ```ts export declare function struct

>>( properties: P -): Encoder<{ [K in keyof P]: OutputOf }, { [K in keyof P]: TypeOf }> +): Encoder }>, { [K in keyof P]: TypeOf }> ``` Added in v2.2.15 diff --git a/dtslint/ts3.5/Codec.ts b/dtslint/ts3.5/Codec.ts index 677e7fe0..257a183e 100644 --- a/dtslint/ts3.5/Codec.ts +++ b/dtslint/ts3.5/Codec.ts @@ -1,12 +1,14 @@ import * as _ from '../../src/Codec' +declare const Optional: (codec: _.Codec) => _.Codec + declare const NumberFromString: _.Codec // // fromStruct // -// $ExpectType Codec<{ a: unknown; b: { c: string; }; }, { a: string; b: { c: string; }; }, { a: string; b: { c: number; }; }> +// $ExpectType Codec<{ b: { c: string; }; a?: unknown; }, { a: string; b: { c: string; }; }, { a: string; b: { c: number; }; }> _.fromStruct({ a: _.string, b: _.fromStruct({ @@ -14,6 +16,14 @@ _.fromStruct({ }) }) +// $ExpectType Codec<{ b: { c?: string | undefined; }; a?: unknown; }, { b: { c?: string | undefined; }; a?: string | undefined; }, { b: { c?: number | undefined; }; a?: string | undefined; }> +_.fromStruct({ + a: Optional(_.string), + b: _.fromStruct({ + c: Optional(NumberFromString) + }) +}) + // // struct // @@ -26,6 +36,14 @@ _.struct({ }) }) +// $ExpectType Codec +_.struct({ + a: Optional(_.string), + b: _.struct({ + c: Optional(_.number), + }), +}) + // // fromPartial // @@ -96,7 +114,7 @@ _.tuple(_.string, _.number, _.boolean) // fromSum // -// $ExpectType Codec<{ _tag: unknown; a: unknown; } | { _tag: unknown; b: string; }, { _tag: "A"; a: string; } | { _tag: "B"; b: string; }, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }> +// $ExpectType Codec<{ a?: unknown; _tag?: unknown; } | { b: string; _tag?: unknown; }, { a: string; _tag: "A"; } | { b: string; _tag: "B"; }, { a: string; _tag: "A"; } | { b: number; _tag: "B"; }> _.fromSum('_tag')({ A: _.fromStruct({ _tag: _.literal('A'), a: _.string }), B: _.fromStruct({ _tag: _.literal('B'), b: NumberFromString }) @@ -109,7 +127,7 @@ _.fromSum('_tag')({ const S1 = _.struct({ _tag: _.literal('A'), a: _.string }) const S2 = _.struct({ _tag: _.literal('B'), b: _.number }) -// $ExpectType Codec +// $ExpectType Codec _.sum('_tag')({ A: S1, B: S2 }) // // $ExpectError // _.sum('_tag')({ A: S1, B: S1 }) diff --git a/dtslint/ts3.5/Decoder.ts b/dtslint/ts3.5/Decoder.ts index 94e80ca7..fd2d03ca 100644 --- a/dtslint/ts3.5/Decoder.ts +++ b/dtslint/ts3.5/Decoder.ts @@ -3,6 +3,8 @@ import * as DE from '../../src/DecodeError' import * as _ from '../../src/Decoder' import * as FS from '../../src/FreeSemigroup' +declare const Optional: (decoder: _.Decoder) => _.Decoder + declare const NumberFromString: _.Decoder // @@ -43,6 +45,14 @@ _.struct({ }) }) +// $ExpectType Decoder +_.struct({ + a: Optional(_.string), + b: _.struct({ + c: Optional(_.number) + }) +}) + // // fromPartial // @@ -113,7 +123,7 @@ _.tuple(_.string, _.number, _.boolean) // fromSum // -// $ExpectType Decoder<{ _tag: unknown; a: unknown; } | { _tag: unknown; b: string; }, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }> +// $ExpectType Decoder<{ _tag: unknown; a: unknown; } | { _tag: unknown; b: string; }, { a: string; _tag: "A"; } | { b: number; _tag: "B"; }> _.fromSum('_tag')({ A: _.fromStruct({ _tag: _.literal('A'), a: _.string }), B: _.fromStruct({ _tag: _.literal('B'), b: NumberFromString }) @@ -126,7 +136,7 @@ _.fromSum('_tag')({ const S1 = _.struct({ _tag: _.literal('A'), a: _.string }) const S2 = _.struct({ _tag: _.literal('B'), b: _.number }) -// $ExpectType Decoder +// $ExpectType Decoder _.sum('_tag')({ A: S1, B: S2 }) // $ExpectError _.sum('_tag')({ A: S1, B: S1 }) diff --git a/dtslint/ts3.5/Encoder.ts b/dtslint/ts3.5/Encoder.ts index 8bb901d4..327aae9f 100644 --- a/dtslint/ts3.5/Encoder.ts +++ b/dtslint/ts3.5/Encoder.ts @@ -1,6 +1,8 @@ import * as E from '../../src/Encoder' import { pipe } from 'fp-ts/lib/pipeable' +declare const Optional: (encoder: E.Encoder) => E.Encoder + const NumberToString: E.Encoder = { encode: String } @@ -30,6 +32,7 @@ E.nullable(NumberToString) // $ExpectType Encoder // struct // E.struct({ a: E.struct({ b: NumberToString }) }) // $ExpectType Encoder<{ a: { b: string; }; }, { a: { b: number; }; }> +E.struct({ a: Optional(E.struct({ b: Optional(NumberToString) })) }) // $ExpectType Encoder<{ a?: { b?: string | undefined; } | undefined; }, { a: { b: number | undefined; } | undefined; }> // // partial @@ -65,7 +68,7 @@ const S1 = E.struct({ _tag: E.id<'A'>(), a: NumberToString }) const S2 = E.struct({ _tag: E.id<'B'>(), b: BooleanToNumber }) const sum = E.sum('_tag') -// $ExpectType Encoder<{ _tag: "A"; a: string; } | { _tag: "B"; b: number; }, { _tag: "A"; a: number; } | { _tag: "B"; b: boolean; }> +// $ExpectType Encoder<{ a: string; _tag: "A"; } | { b: number; _tag: "B"; }, { _tag: "A"; a: number; } | { _tag: "B"; b: boolean; }> sum({ A: S1, B: S2 }) const S3 = E.struct({ _tag: E.id<'C'>(), c: E.id() }) diff --git a/src/Codec.ts b/src/Codec.ts index fe43c2aa..bcc1da6b 100644 --- a/src/Codec.ts +++ b/src/Codec.ts @@ -148,8 +148,12 @@ export function nullable(or: Codec): Codec>>( properties: P -): Codec<{ [K in keyof P]: InputOf }, { [K in keyof P]: OutputOf }, { [K in keyof P]: TypeOf }> { - return make(D.fromStruct(properties) as any, E.struct(properties)) +): Codec< + ToOptional<{ [K in keyof P]: InputOf }>, + ToOptional<{ [K in keyof P]: OutputOf }>, + ToOptional<{ [K in keyof P]: TypeOf }> +> { + return make(D.fromStruct(properties) as any, E.struct(properties) as any) } /** @@ -167,7 +171,7 @@ export const fromType = fromStruct */ export function struct

>>( properties: P -): Codec }, { [K in keyof P]: TypeOf }> { +): Codec }>, ToOptional<{ [K in keyof P]: TypeOf }>> { return pipe(UnknownRecord, compose(fromStruct(properties as any))) as any } @@ -385,3 +389,10 @@ export type OutputOf = E.OutputOf * @since 2.2.3 */ export type TypeOf = E.TypeOf + +type UndefinedProperties = { + [P in keyof T]-?: undefined extends T[P] ? P : never +}[keyof T] +type ToOptional = Merge>> & Partial>>> +type Identity = T +type Merge = T extends any ? Identity<{ [k in keyof T]: T[k] }> : never diff --git a/src/Decoder.ts b/src/Decoder.ts index f98ff1d2..7751774b 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -232,8 +232,8 @@ export const nullable: (or: Decoder) => Decoder */ export const fromStruct =

>>( properties: P -): Decoder<{ [K in keyof P]: InputOf }, { [K in keyof P]: TypeOf }> => - K.fromStruct(M)((k, e) => FS.of(DE.key(k, DE.required, e)))(properties) +): Decoder<{ [K in keyof P]: InputOf }, ToOptional<{ [K in keyof P]: TypeOf }>> => + K.fromStruct(M)((k, e) => FS.of(DE.key(k, DE.required, e)))(properties) as any /** * Use `fromStruct` instead. @@ -250,7 +250,8 @@ export const fromType = fromStruct */ export const struct = ( properties: { [K in keyof A]: Decoder } -): Decoder => pipe(UnknownRecord as any, compose(fromStruct(properties))) +): Decoder> => + pipe(UnknownRecord as any, compose(fromStruct(properties))) as any /** * Use `struct` instead. @@ -488,8 +489,8 @@ export const Schemable: S.Schemable2C = { number, boolean, nullable, - type, - struct, + type: type as S.Schemable2C['type'], // tslint:disable-line:deprecation + struct: struct as S.Schemable2C['struct'], partial, record, array, @@ -609,3 +610,10 @@ export const draw = (e: DecodeError): string => toForest(e).map(drawTree).join(' export const stringify: (e: E.Either) => string = /*#__PURE__*/ E.fold(draw, (a) => JSON.stringify(a, null, 2)) + +type UndefinedProperties = { + [P in keyof T]-?: undefined extends T[P] ? P : never +}[keyof T] +type ToOptional = Merge>> & Partial>>> +type Identity = T +type Merge = T extends any ? Identity<{ [k in keyof T]: T[k] }> : never diff --git a/src/Encoder.ts b/src/Encoder.ts index 8cf7a085..b28cb9de 100644 --- a/src/Encoder.ts +++ b/src/Encoder.ts @@ -45,14 +45,14 @@ export function nullable(or: Encoder): Encoder { */ export function struct

>>( properties: P -): Encoder<{ [K in keyof P]: OutputOf }, { [K in keyof P]: TypeOf }> { +): Encoder }>, { [K in keyof P]: TypeOf }> { return { encode: (a) => { const o: Record = {} as any for (const k in properties) { o[k] = properties[k].encode(a[k]) } - return o + return o as any } } } @@ -260,3 +260,10 @@ export type TypeOf = E extends Encoder ? A : never * @since 2.2.3 */ export type OutputOf = E extends Encoder ? O : never + +type UndefinedProperties = { + [P in keyof T]-?: undefined extends T[P] ? P : never +}[keyof T] +type ToOptional = Merge>> & Partial>>> +type Identity = T +type Merge = T extends any ? Identity<{ [k in keyof T]: T[k] }> : never