From 3bf77a162e25fb49cb94bbf2f129c3f2535baeeb Mon Sep 17 00:00:00 2001 From: mmkal Date: Tue, 22 Jan 2019 20:04:51 -0500 Subject: [PATCH 1/8] whitespace change to start pull request --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 7ccf157d6..2f717e0df 100644 --- a/src/index.ts +++ b/src/index.ts @@ -826,7 +826,7 @@ export interface TypeC

* @alias `interface` * @since 1.0.0 */ -export const type =

(props: P, name: string = getNameFromProps(props)): TypeC

=> { +export const type =

(props: P, name: string = getNameFromProps(props)): TypeC

=> { const keys = Object.keys(props) const types = keys.map(key => props[key]) const len = keys.length From 1f675d4f456e0ecbda660c826206695fde396477 Mon Sep 17 00:00:00 2001 From: mmkal Date: Tue, 22 Jan 2019 20:57:19 -0500 Subject: [PATCH 2/8] add partialPartial POC --- dtslint/index.ts | 11 +++++++++++ src/index.ts | 44 ++++++++++++++++++++++++++++++++++++++++-- test/partialPartial.ts | 18 +++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 test/partialPartial.ts diff --git a/dtslint/index.ts b/dtslint/index.ts index 959b15727..0e1d5f958 100644 --- a/dtslint/index.ts +++ b/dtslint/index.ts @@ -587,3 +587,14 @@ declare function withValidation( declare const fa: TaskEither withValidation(t.void, () => 'validation error', fa) + +const Person = t.partialPartial({ + name: t.string, + age: t.optional(t.number), +}) +type Person = t.TypeOf + +const bob: Person = { name: 'Bob' } +const bob30: Person = { name: 'Bob', age: 30 } +// $ExpectError +const nobody: Person = {} diff --git a/src/index.ts b/src/index.ts index 2f717e0df..ef748e62e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -820,13 +820,53 @@ export interface Props { * @since 1.5.3 */ export interface TypeC

- extends InterfaceType }, { [K in keyof P]: OutputOf }, unknown> {} +extends InterfaceType }, { [K in keyof P]: OutputOf }, unknown> {} + +interface Optional { + optional: true +} + +export const optional = (rt: Type, name?: string) => { + const unionType = union([rt, nullType, undefinedType], name) + return Object.assign(unionType, { optional: true } as Optional) +} + +type OptionalPropsKeys

= { [K in keyof P]: P[K] extends Optional ? K : never }[keyof P] +type OptionalPropsTypes

= { [K in OptionalPropsKeys

]?: P[K] extends Optional ? P[K]['_A'] : never } +type OptionalPropsOutputs

= { + [K in OptionalPropsKeys

]?: P[K] extends Optional ? P[K]['_O'] : never +} +type RequiredPropsKeys

= { [K in keyof P]: P[K] extends Optional ? never : K }[keyof P] +type RequiredPropsTypes

= { [K in RequiredPropsKeys

]: P[K] extends Optional ? never : P[K]['_A'] } +type RequiredPropsOutputs

= { [K in RequiredPropsKeys

]: P[K] extends Optional ? never : P[K]['_O'] } + +export const partialPartial =

( + props: P, + name?: string +): Type & RequiredPropsTypes

, OptionalPropsOutputs

& RequiredPropsOutputs

, mixed> => { + let someOptional = false + const [optionalProps, requiredProps] = [{}, {}] as Props[] + for (const key of Object.keys(props)) { + const val: any = props[key] + if (val.optional) { + someOptional = true + optionalProps[key] = val + } else { + requiredProps[key] = val + } + } + if (someOptional) { + return intersection([type(requiredProps), partial(optionalProps)], name) as any + } else { + return type(props, name) as any + } +} /** * @alias `interface` * @since 1.0.0 */ -export const type =

(props: P, name: string = getNameFromProps(props)): TypeC

=> { +export const type =

(props: P, name: string = getNameFromProps(props)): TypeC

=> { const keys = Object.keys(props) const types = keys.map(key => props[key]) const len = keys.length diff --git a/test/partialPartial.ts b/test/partialPartial.ts new file mode 100644 index 000000000..e837c337e --- /dev/null +++ b/test/partialPartial.ts @@ -0,0 +1,18 @@ +import * as t from '../src/index' +import { Right, Left } from 'fp-ts/lib/Either' + +describe('partialPartial', () => { + it('handles some optional props', () => { + const Person = t.partialPartial({ name: t.string, age: t.optional(t.number) }) + expect(Person.decode({ name: 'bob', age: 30 })).toBeInstanceOf(Right) + expect(Person.decode({ name: 'bob' })).toBeInstanceOf(Right) + expect(Person.decode({})).toBeInstanceOf(Left) + }) + + it('handles all required props', () => { + const Person = t.partialPartial({ name: t.string, age: t.number }) + expect(Person.decode({ name: 'bob', age: 30 })).toBeInstanceOf(Right) + expect(Person.decode({ name: 'bob' })).toBeInstanceOf(Left) + expect(Person.decode({})).toBeInstanceOf(Left) + }) +}) From 223d046d292d7df59415727a7efbc9ba79f506b7 Mon Sep 17 00:00:00 2001 From: mmkal Date: Tue, 22 Jan 2019 21:08:05 -0500 Subject: [PATCH 3/8] fix whitespace --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index ef748e62e..256f2764f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -820,7 +820,7 @@ export interface Props { * @since 1.5.3 */ export interface TypeC

-extends InterfaceType }, { [K in keyof P]: OutputOf }, unknown> {} + extends InterfaceType }, { [K in keyof P]: OutputOf }, unknown> {} interface Optional { optional: true From 50d8017ba319992a7c9877e3a8061bdfa5db3d49 Mon Sep 17 00:00:00 2001 From: mmkal Date: Tue, 22 Jan 2019 21:33:41 -0500 Subject: [PATCH 4/8] use fewer type ternaries --- src/index.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 256f2764f..976027577 100644 --- a/src/index.ts +++ b/src/index.ts @@ -832,13 +832,11 @@ export const optional = (rt: Type, name?: string) => { } type OptionalPropsKeys

= { [K in keyof P]: P[K] extends Optional ? K : never }[keyof P] -type OptionalPropsTypes

= { [K in OptionalPropsKeys

]?: P[K] extends Optional ? P[K]['_A'] : never } -type OptionalPropsOutputs

= { - [K in OptionalPropsKeys

]?: P[K] extends Optional ? P[K]['_O'] : never -} +type OptionalPropsTypes

= { [K in OptionalPropsKeys

]?: P[K]['_A'] } +type OptionalPropsOutputs

= { [K in OptionalPropsKeys

]?: P[K]['_O'] } type RequiredPropsKeys

= { [K in keyof P]: P[K] extends Optional ? never : K }[keyof P] -type RequiredPropsTypes

= { [K in RequiredPropsKeys

]: P[K] extends Optional ? never : P[K]['_A'] } -type RequiredPropsOutputs

= { [K in RequiredPropsKeys

]: P[K] extends Optional ? never : P[K]['_O'] } +type RequiredPropsTypes

= { [K in RequiredPropsKeys

]: P[K]['_A'] } +type RequiredPropsOutputs

= { [K in RequiredPropsKeys

]: P[K]['_O'] } export const partialPartial =

( props: P, From c9ac5f681ea44b158d4b3d1b405152733ba20421 Mon Sep 17 00:00:00 2001 From: mmkal Date: Wed, 23 Jan 2019 08:02:02 -0500 Subject: [PATCH 5/8] use keyof for optional props and outputs also add example using abstract type --- dtslint/index.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 5 ++--- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/dtslint/index.ts b/dtslint/index.ts index 0e1d5f958..24b1e4be3 100644 --- a/dtslint/index.ts +++ b/dtslint/index.ts @@ -598,3 +598,53 @@ const bob: Person = { name: 'Bob' } const bob30: Person = { name: 'Bob', age: 30 } // $ExpectError const nobody: Person = {} + +/** + * if a generic type is passed into a method which uses partialPartial, it could either + * be optional or not, so if specifying the return type explicitly, we have to assume + * it could be undefined on the `TypeOf` type. + */ +export const partialPartialAbstract = , A = any, O = any>( + type: Type +): t.Validation<{ t: 'v'; v?: t.TypeOf }> => { + const T = t.partialPartial({ + t: t.literal('v'), + v: type + }) + const res = T.validate(1 as any, []) + res.map(x => { + x.t // $ExpectType "v" + // `x.v` could be undefined; we don't know whether `type` is optional here: + x.v // $ExpectType Type["_A"] | undefined + }) + return res +} + +/** + * to avoid the problem above, rely on type inference, which will keep track of the + * generic type to figure out if `v` should be optional. + */ +export const partialPartialAbstractInferred = , A = any, O = any>( + type: Type +) => { + const T = t.partialPartial({ + t: t.literal('v'), + v: type + }) + const res = T.validate(1 as any, []) + res.map(x => { + x.t // $ExpectType "v" + // `x.v` could be undefined; we don't know whether `type` is optional here: + x.v // $ExpectType Type["_A"] | undefined + }) + return res +} + +const inferredWithOptional = partialPartialAbstractInferred(t.optional(t.string)) +const vShouldBeOptional = inferredWithOptional.getOrElse(0 as any) +const vNotSuppliedOk: typeof vShouldBeOptional = { t: 'v' } + +const inferredWithoutOptional = partialPartialAbstractInferred(t.string) +const vShouldBeRequired = inferredWithoutOptional.getOrElse(0 as any) +// $ExpectError +const vNotSuppliedFailure: typeof vShouldBeRequired = { t: 'v' } diff --git a/src/index.ts b/src/index.ts index 976027577..950f5eb1a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -831,9 +831,8 @@ export const optional = (rt: Type, name?: string) => { return Object.assign(unionType, { optional: true } as Optional) } -type OptionalPropsKeys

= { [K in keyof P]: P[K] extends Optional ? K : never }[keyof P] -type OptionalPropsTypes

= { [K in OptionalPropsKeys

]?: P[K]['_A'] } -type OptionalPropsOutputs

= { [K in OptionalPropsKeys

]?: P[K]['_O'] } +type OptionalPropsTypes

= { [K in keyof P]?: P[K]['_A'] } +type OptionalPropsOutputs

= { [K in keyof P]?: P[K]['_O'] } type RequiredPropsKeys

= { [K in keyof P]: P[K] extends Optional ? never : K }[keyof P] type RequiredPropsTypes

= { [K in RequiredPropsKeys

]: P[K]['_A'] } type RequiredPropsOutputs

= { [K in RequiredPropsKeys

]: P[K]['_O'] } From 8597b1a111036e8d71db07818ef4024c9bdea9d9 Mon Sep 17 00:00:00 2001 From: mmkal Date: Wed, 23 Jan 2019 21:40:31 -0500 Subject: [PATCH 6/8] optimisation for all optional props --- src/index.ts | 9 +++++++-- test/partialPartial.ts | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 950f5eb1a..4bbf79b77 100644 --- a/src/index.ts +++ b/src/index.ts @@ -842,18 +842,23 @@ export const partialPartial =

( name?: string ): Type & RequiredPropsTypes

, OptionalPropsOutputs

& RequiredPropsOutputs

, mixed> => { let someOptional = false - const [optionalProps, requiredProps] = [{}, {}] as Props[] + let someRequired = false + const optionalProps: Props = {} + const requiredProps: Props = {} for (const key of Object.keys(props)) { const val: any = props[key] if (val.optional) { someOptional = true optionalProps[key] = val } else { + someRequired = true requiredProps[key] = val } } - if (someOptional) { + if (someOptional && someRequired) { return intersection([type(requiredProps), partial(optionalProps)], name) as any + } else if (someOptional) { + return partial(props, name) as any } else { return type(props, name) as any } diff --git a/test/partialPartial.ts b/test/partialPartial.ts index e837c337e..1b47a7c7e 100644 --- a/test/partialPartial.ts +++ b/test/partialPartial.ts @@ -4,6 +4,7 @@ import { Right, Left } from 'fp-ts/lib/Either' describe('partialPartial', () => { it('handles some optional props', () => { const Person = t.partialPartial({ name: t.string, age: t.optional(t.number) }) + expect(Person.decode({ name: 'bob', age: 'thirty' })).toBeInstanceOf(Left) expect(Person.decode({ name: 'bob', age: 30 })).toBeInstanceOf(Right) expect(Person.decode({ name: 'bob' })).toBeInstanceOf(Right) expect(Person.decode({})).toBeInstanceOf(Left) @@ -11,8 +12,17 @@ describe('partialPartial', () => { it('handles all required props', () => { const Person = t.partialPartial({ name: t.string, age: t.number }) + expect(Person.decode({ name: 'bob', age: 'thirty' })).toBeInstanceOf(Left) expect(Person.decode({ name: 'bob', age: 30 })).toBeInstanceOf(Right) expect(Person.decode({ name: 'bob' })).toBeInstanceOf(Left) expect(Person.decode({})).toBeInstanceOf(Left) }) + + it('handles all optional props', () => { + const Person = t.partialPartial({ name: t.optional(t.string), age: t.optional(t.number) }) + expect(Person.decode({ name: 'bob', age: 'thirty' })).toBeInstanceOf(Left) + expect(Person.decode({ name: 'bob', age: 30 })).toBeInstanceOf(Right) + expect(Person.decode({ name: 'bob' })).toBeInstanceOf(Right) + expect(Person.decode({})).toBeInstanceOf(Right) + }) }) From 3356d5f034d0ed57efbef4958260e0c36226375a Mon Sep 17 00:00:00 2001 From: mmkal Date: Wed, 23 Jan 2019 23:04:37 -0500 Subject: [PATCH 7/8] make sure optional types don't allow `any` --- dtslint/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dtslint/index.ts b/dtslint/index.ts index 24b1e4be3..2e129afd5 100644 --- a/dtslint/index.ts +++ b/dtslint/index.ts @@ -597,6 +597,8 @@ type Person = t.TypeOf const bob: Person = { name: 'Bob' } const bob30: Person = { name: 'Bob', age: 30 } // $ExpectError +const bobThirty: Person = { name: 'Bob', age: 'thirty' } +// $ExpectError const nobody: Person = {} /** From 99ececf42e251545e08bc6ffc956a3d781f632d9 Mon Sep 17 00:00:00 2001 From: mmkal Date: Tue, 5 Feb 2019 10:15:39 -0500 Subject: [PATCH 8/8] use ? in optional name This waygetInterfaceTypeName can be used --- src/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index d9dfeaee7..22bbd129f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -800,7 +800,7 @@ interface Optional { } export const optional = (rt: Type, name?: string) => { - const unionType = union([rt, nullType, undefinedType], name) + const unionType = union([rt, nullType, undefinedType], name || rt.name + '?') return Object.assign(unionType, { optional: true } as Optional) } @@ -828,12 +828,13 @@ export const partialPartial =

( requiredProps[key] = val } } + const computedName = name || getInterfaceTypeName(props) if (someOptional && someRequired) { - return intersection([type(requiredProps), partial(optionalProps)], name) as any + return intersection([type(requiredProps), partial(optionalProps)], computedName) as any } else if (someOptional) { - return partial(props, name) as any + return partial(props, computedName) as any } else { - return type(props, name) as any + return type(props, computedName) as any } }