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

Treat struct properties that have type undefined as optional #639

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/modules/Codec.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,11 @@ Added in v2.2.3
```ts
export declare function fromStruct<P extends Record<string, Codec<any, any, any>>>(
properties: P
): Codec<{ [K in keyof P]: InputOf<P[K]> }, { [K in keyof P]: OutputOf<P[K]> }, { [K in keyof P]: TypeOf<P[K]> }>
): Codec<
ToOptional<{ [K in keyof P]: InputOf<P[K]> }>,
ToOptional<{ [K in keyof P]: OutputOf<P[K]> }>,
ToOptional<{ [K in keyof P]: TypeOf<P[K]> }>
>
```

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

Added in v2.2.15
Expand Down
48 changes: 44 additions & 4 deletions docs/modules/Decoder.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,21 @@ Added in v2.2.8
```ts
export declare const fromStruct: <P extends Record<string, Decoder<any, any>>>(
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<keyof P, UndefinedProperties<{ [K in keyof P]: K.TypeOf<'Either', P[K]> }>>
> &
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
Expand Down Expand Up @@ -371,7 +385,13 @@ Added in v2.2.7
```ts
export declare const struct: <A>(
properties: { [K in keyof A]: Decoder<unknown, A[K]> }
) => Decoder<unknown, { [K in keyof A]: A[K] }>
) => Decoder<
unknown,
Merge<
Pick<{ [K in keyof A]: A[K] }, Exclude<keyof A, UndefinedProperties<{ [K in keyof A]: A[K] }>>> &
Partial<Pick<{ [K in keyof A]: A[K] }, UndefinedProperties<{ [K in keyof A]: A[K] }>>>
>
>
```

Added in v2.2.15
Expand Down Expand Up @@ -433,7 +453,21 @@ Use `fromStruct` instead.
```ts
export declare const fromType: <P extends Record<string, Decoder<any, any>>>(
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<keyof P, UndefinedProperties<{ [K in keyof P]: K.TypeOf<'Either', P[K]> }>>
> &
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
Expand All @@ -447,7 +481,13 @@ Use `struct` instead.
```ts
export declare const type: <A>(
properties: { [K in keyof A]: Decoder<unknown, A[K]> }
) => Decoder<unknown, { [K in keyof A]: A[K] }>
) => Decoder<
unknown,
Merge<
Pick<{ [K in keyof A]: A[K] }, Exclude<keyof A, UndefinedProperties<{ [K in keyof A]: A[K] }>>> &
Partial<Pick<{ [K in keyof A]: A[K] }, UndefinedProperties<{ [K in keyof A]: A[K] }>>>
>
>
```

Added in v2.2.7
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/Encoder.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ Added in v2.2.3
```ts
export declare function struct<P extends Record<string, Encoder<any, any>>>(
properties: P
): Encoder<{ [K in keyof P]: OutputOf<P[K]> }, { [K in keyof P]: TypeOf<P[K]> }>
): Encoder<ToOptional<{ [K in keyof P]: OutputOf<P[K]> }>, { [K in keyof P]: TypeOf<P[K]> }>
```

Added in v2.2.15
Expand Down
24 changes: 21 additions & 3 deletions dtslint/ts3.5/Codec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import * as _ from '../../src/Codec'

declare const Optional: <I, O, A>(codec: _.Codec<I, O, A>) => _.Codec<I | undefined, O | undefined, A | undefined>

declare const NumberFromString: _.Codec<string, string, number>

//
// 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({
c: NumberFromString
})
})

// $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
//
Expand All @@ -26,6 +36,14 @@ _.struct({
})
})

// $ExpectType Codec<unknown, { b: { c?: number | undefined; }; a?: string | undefined; }, { b: { c?: number | undefined; }; a?: string | undefined; }>
_.struct({
a: Optional(_.string),
b: _.struct({
c: Optional(_.number),
}),
})

//
// fromPartial
//
Expand Down Expand Up @@ -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 })
Expand All @@ -109,7 +127,7 @@ _.fromSum('_tag')({
const S1 = _.struct({ _tag: _.literal('A'), a: _.string })
const S2 = _.struct({ _tag: _.literal('B'), b: _.number })

// $ExpectType Codec<unknown, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
// $ExpectType Codec<unknown, { a: string; _tag: "A"; } | { b: number; _tag: "B"; }, { a: string; _tag: "A"; } | { b: number; _tag: "B"; }>
Comment on lines -112 to +130
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't expecting these to be reordered.

_.sum('_tag')({ A: S1, B: S2 })
// // $ExpectError
// _.sum('_tag')({ A: S1, B: S1 })
14 changes: 12 additions & 2 deletions dtslint/ts3.5/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as DE from '../../src/DecodeError'
import * as _ from '../../src/Decoder'
import * as FS from '../../src/FreeSemigroup'

declare const Optional: <I, A>(decoder: _.Decoder<I, A>) => _.Decoder<I, A | undefined>

declare const NumberFromString: _.Decoder<string, number>

//
Expand Down Expand Up @@ -43,6 +45,14 @@ _.struct({
})
})

// $ExpectType Decoder<unknown, { b: { c?: number | undefined; }; a?: string | undefined; }>
_.struct({
a: Optional(_.string),
b: _.struct({
c: Optional(_.number)
})
})

//
// fromPartial
//
Expand Down Expand Up @@ -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 })
Expand All @@ -126,7 +136,7 @@ _.fromSum('_tag')({
const S1 = _.struct({ _tag: _.literal('A'), a: _.string })
const S2 = _.struct({ _tag: _.literal('B'), b: _.number })

// $ExpectType Decoder<unknown, { _tag: "A"; a: string; } | { _tag: "B"; b: number; }>
// $ExpectType Decoder<unknown, { a: string; _tag: "A"; } | { b: number; _tag: "B"; }>
_.sum('_tag')({ A: S1, B: S2 })
// $ExpectError
_.sum('_tag')({ A: S1, B: S1 })
Expand Down
5 changes: 4 additions & 1 deletion dtslint/ts3.5/Encoder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as E from '../../src/Encoder'
import { pipe } from 'fp-ts/lib/pipeable'

declare const Optional: <O, A>(encoder: E.Encoder<O, A>) => E.Encoder<O | undefined, A | undefined>

const NumberToString: E.Encoder<string, number> = {
encode: String
}
Expand Down Expand Up @@ -30,6 +32,7 @@ E.nullable(NumberToString) // $ExpectType Encoder<string | null, number | null>
// 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
Expand Down Expand Up @@ -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<string>() })
Expand Down
17 changes: 14 additions & 3 deletions src/Codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,12 @@ export function nullable<I, O, A>(or: Codec<I, O, A>): Codec<null | I, null | O,
*/
export function fromStruct<P extends Record<string, Codec<any, any, any>>>(
properties: P
): Codec<{ [K in keyof P]: InputOf<P[K]> }, { [K in keyof P]: OutputOf<P[K]> }, { [K in keyof P]: TypeOf<P[K]> }> {
return make(D.fromStruct(properties) as any, E.struct(properties))
): Codec<
ToOptional<{ [K in keyof P]: InputOf<P[K]> }>,
ToOptional<{ [K in keyof P]: OutputOf<P[K]> }>,
ToOptional<{ [K in keyof P]: TypeOf<P[K]> }>
> {
return make(D.fromStruct(properties) as any, E.struct(properties) as any)
}

/**
Expand All @@ -167,7 +171,7 @@ export const fromType = fromStruct
*/
export function struct<P extends Record<string, Codec<unknown, any, any>>>(
properties: P
): Codec<unknown, { [K in keyof P]: OutputOf<P[K]> }, { [K in keyof P]: TypeOf<P[K]> }> {
): Codec<unknown, ToOptional<{ [K in keyof P]: OutputOf<P[K]> }>, ToOptional<{ [K in keyof P]: TypeOf<P[K]> }>> {
return pipe(UnknownRecord, compose(fromStruct(properties as any))) as any
}

Expand Down Expand Up @@ -385,3 +389,10 @@ export type OutputOf<C> = E.OutputOf<C>
* @since 2.2.3
*/
export type TypeOf<C> = E.TypeOf<C>

type UndefinedProperties<T> = {
[P in keyof T]-?: undefined extends T[P] ? P : never
}[keyof T]
type ToOptional<T> = Merge<Pick<T, Exclude<keyof T, UndefinedProperties<T>>> & Partial<Pick<T, UndefinedProperties<T>>>>
Comment on lines +393 to +396
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type Identity<T> = T
type Merge<T> = T extends any ? Identity<{ [k in keyof T]: T[k] }> : never
Comment on lines +397 to +398
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

18 changes: 13 additions & 5 deletions src/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ export const nullable: <I, A>(or: Decoder<I, A>) => Decoder<null | I, null | A>
*/
export const fromStruct = <P extends Record<string, Decoder<any, any>>>(
properties: P
): Decoder<{ [K in keyof P]: InputOf<P[K]> }, { [K in keyof P]: TypeOf<P[K]> }> =>
K.fromStruct(M)((k, e) => FS.of(DE.key(k, DE.required, e)))(properties)
): Decoder<{ [K in keyof P]: InputOf<P[K]> }, ToOptional<{ [K in keyof P]: TypeOf<P[K]> }>> =>
K.fromStruct(M)((k, e) => FS.of(DE.key(k, DE.required, e)))(properties) as any

/**
* Use `fromStruct` instead.
Expand All @@ -250,7 +250,8 @@ export const fromType = fromStruct
*/
export const struct = <A>(
properties: { [K in keyof A]: Decoder<unknown, A[K]> }
): Decoder<unknown, { [K in keyof A]: A[K] }> => pipe(UnknownRecord as any, compose(fromStruct(properties)))
): Decoder<unknown, ToOptional<{ [K in keyof A]: A[K] }>> =>
pipe(UnknownRecord as any, compose(fromStruct(properties))) as any

/**
* Use `struct` instead.
Expand Down Expand Up @@ -488,8 +489,8 @@ export const Schemable: S.Schemable2C<URI, unknown> = {
number,
boolean,
nullable,
type,
struct,
type: type as S.Schemable2C<URI, unknown>['type'], // tslint:disable-line:deprecation
struct: struct as S.Schemable2C<URI, unknown>['struct'],
partial,
record,
array,
Expand Down Expand Up @@ -609,3 +610,10 @@ export const draw = (e: DecodeError): string => toForest(e).map(drawTree).join('
export const stringify: <A>(e: E.Either<DecodeError, A>) => string =
/*#__PURE__*/
E.fold(draw, (a) => JSON.stringify(a, null, 2))

type UndefinedProperties<T> = {
[P in keyof T]-?: undefined extends T[P] ? P : never
}[keyof T]
type ToOptional<T> = Merge<Pick<T, Exclude<keyof T, UndefinedProperties<T>>> & Partial<Pick<T, UndefinedProperties<T>>>>
type Identity<T> = T
type Merge<T> = T extends any ? Identity<{ [k in keyof T]: T[k] }> : never
11 changes: 9 additions & 2 deletions src/Encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ export function nullable<O, A>(or: Encoder<O, A>): Encoder<null | O, null | A> {
*/
export function struct<P extends Record<string, Encoder<any, any>>>(
properties: P
): Encoder<{ [K in keyof P]: OutputOf<P[K]> }, { [K in keyof P]: TypeOf<P[K]> }> {
): Encoder<ToOptional<{ [K in keyof P]: OutputOf<P[K]> }>, { [K in keyof P]: TypeOf<P[K]> }> {
return {
encode: (a) => {
const o: Record<keyof P, any> = {} as any
for (const k in properties) {
o[k] = properties[k].encode(a[k])
}
return o
return o as any
}
}
}
Expand Down Expand Up @@ -260,3 +260,10 @@ export type TypeOf<E> = E extends Encoder<any, infer A> ? A : never
* @since 2.2.3
*/
export type OutputOf<E> = E extends Encoder<infer O, any> ? O : never

type UndefinedProperties<T> = {
[P in keyof T]-?: undefined extends T[P] ? P : never
}[keyof T]
type ToOptional<T> = Merge<Pick<T, Exclude<keyof T, UndefinedProperties<T>>> & Partial<Pick<T, UndefinedProperties<T>>>>
type Identity<T> = T
type Merge<T> = T extends any ? Identity<{ [k in keyof T]: T[k] }> : never