From 4a9c94fcc07d025589bc4766f9cf67734059ad86 Mon Sep 17 00:00:00 2001 From: Matthew Burghoffer Date: Thu, 18 Aug 2022 17:16:33 -0700 Subject: [PATCH 1/3] adding SemiPartialType and associated tests --- src/index.ts | 329 +++++++++++++++++++++++++++++++++++++ test/2.1.x/semi-partial.ts | 289 ++++++++++++++++++++++++++++++++ 2 files changed, 618 insertions(+) create mode 100644 test/2.1.x/semi-partial.ts diff --git a/src/index.ts b/src/index.ts index 33b81e21e..e32dce40b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1495,6 +1495,335 @@ export function partial

( ) } +export type OptionalTypedProp = { + type: Type + optional: true +} + +export type RequiredTypedProp = { + type: Type + optional?: false +} + +export type TypedProp = OptionalTypedProp | RequiredTypedProp + +export type AnyProp = Type | TypedProp + +export type SemiProps = { + [key: string]: AnyProp +} + +export type AsType

> = P extends Mixed ? P : P extends TypedProp ? P['type'] : never + +type StrKey = keyof T & string + +export type RequiredProps

= { + [K in StrKey

]: P[K] extends OptionalTypedProp ? never : K +}[StrKey

] + +export type OptionalProps

= { + [K in StrKey

]: P[K] extends OptionalTypedProp ? K : never +}[StrKey

] + +export class SemiPartialType

extends Type { + readonly _tag: 'SemiPartialType' = 'SemiPartialType' + + constructor( + name: string, + is: SemiPartialType['is'], + validate: SemiPartialType['validate'], + encode: SemiPartialType['encode'], + readonly props: P + ) { + super(name, is, validate, encode) + } +} + +type SemiPropsTypes

= { + [K in RequiredProps

]: TypeOf> +} & + { + [K in OptionalProps

]?: TypeOf> | undefined + } + +type SemiPropsOutputs

= { + [K in RequiredProps

]: OutputOf> +} & + { + [K in OptionalProps

]?: OutputOf> | undefined + } + +export interface SemiPartialC

+ extends SemiPartialType< + P, + { [K in keyof SemiPropsTypes

]: SemiPropsTypes

[K] }, + { [K in keyof SemiPropsOutputs

]: SemiPropsOutputs

[K] }, + unknown + > {} + +interface PropsInfo

{ + count: number + keys?: StrKey

[] + types?: AsType]>[] +} + +interface AllPropsInfo

{ + readonly name: string + readonly useIdentity: boolean + readonly required: PropsInfo

+ readonly optional: PropsInfo

+} + +function isTypedProp(prop: AnyProp): prop is TypedProp { + const maybe = prop as Partial> + if (typeof maybe.type !== 'object') { + return false + } + switch (typeof maybe.optional) { + case 'boolean': + case 'undefined': + return true + default: + return false + } +} + +function getPartialPropsInfo

(properties: P, name: string | undefined): AllPropsInfo

{ + let requiredKeys: StrKey

[] | undefined + let requiredTypes: AsType]>[] | undefined + let requiredCount = 0 + + let optionalKeys: StrKey

[] | undefined + let optionalTypes: AsType]>[] | undefined + let optionalCount = 0 + + let useIdentity = true + let needsName = false + if (name === undefined) { + name = '' + needsName = true + } + + for (const [key, value] of Object.entries(properties)) { + const prop = value as P[StrKey

] + const propKey = key as StrKey

+ let propIsOptional: boolean + let propType: AsType]> + + if (isTypedProp(prop)) { + propIsOptional = prop.optional ?? false + propType = prop.type as AsType]> + } else { + propIsOptional = false + propType = prop as AsType]> + } + + if (propType.encode !== identity) { + useIdentity = false + } + + if (propIsOptional) { + if (optionalCount === 0) { + optionalKeys = [] + optionalTypes = [] + } + optionalKeys!.push(propKey) + optionalTypes!.push(propType) + optionalCount += 1 + + if (needsName) { + if (name.length > 0) { + name += ', ' + } + name += `${key}?: ${propType.name}` + } + } else { + if (requiredCount === 0) { + requiredKeys = [] + requiredTypes = [] + } + requiredKeys!.push(propKey) + requiredTypes!.push(propType) + requiredCount += 1 + + if (needsName) { + if (name.length > 0) { + name += ', ' + } + name += `${key}: ${propType.name}` + } + } + } + + if (needsName) { + name = `{ ${name} }` + } + + return { + name, + useIdentity, + required: { + count: requiredCount, + keys: requiredKeys, + types: requiredTypes + }, + optional: { + count: optionalCount, + keys: optionalKeys, + types: optionalTypes + } + } +} + +/** + * Creates a type that allows for some properties to be required and some properties to be optional. + * + * The following examples are interpreted as required properties: + * ```ts + * { + * req1: t.string, + * req2: { + * type: t.string, + * }, + * req3: { + * type: t.string, + * optional: false, + * }, + * } + * ``` + * + * The following example is interpreted as an optional property: + * ```ts + * { + * opt: { + * type: t.string, + * optional: true, + * } + * } + * ``` + */ +export function semiPartial

(properties: P, name?: string): SemiPartialC

{ + const { name: typeName, useIdentity, required, optional } = getPartialPropsInfo(properties, name) + + return new SemiPartialType( + typeName, + (u): u is SemiPartialC

=> { + // is it a record? + if (!UnknownRecord.is(u)) { + return false + } + + // test all the required properties + for (let i = 0; i < required.count; ++i) { + const key = required.keys![i] + const propValue = u[key] + if (propValue === undefined && !hasOwnProperty.call(u, key)) { + return false + } + const type = required.types![i] + if (!type.is(propValue)) { + return false + } + } + + // test all the optional properties + for (let i = 0; i < optional.count; ++i) { + const key = optional.keys![i] + const type = optional.types![i] + const propValue = u[key] + if (propValue !== undefined && !type.is(propValue)) { + return false + } + } + + return true + }, + (u, ctx) => { + // validate it is a record + const urv = UnknownRecord.validate(u, ctx) + if (isLeft(urv)) { + return urv + } + + const rec = urv.right + const errors: Errors = [] + let validRec = rec + + // validate all required properties + for (let i = 0; i < required.count; ++i) { + const key = required.keys![i] + const type = required.types![i] + const propValue = rec[key] + const result = type.validate(propValue, appendContext(ctx, key, type, propValue)) + if (isLeft(result)) { + pushAll(errors, result.left) + continue + } + const validPropValue = result.right + if (validPropValue !== propValue || (validPropValue === undefined && !hasOwnProperty.call(rec, key))) { + if (validRec === rec) { + validRec = { ...rec } + } + validRec[key] = validPropValue + } + } + + // validate all optional properties + for (let i = 0; i < optional.count; ++i) { + const key = optional.keys![i] + const propValue = rec[key] + if (propValue === undefined) { + continue + } + const type = optional.types![i] + const result = type.validate(propValue, appendContext(ctx, key, type, propValue)) + if (isLeft(result)) { + pushAll(errors, result.left) + continue + } + const validPropValue = result.right + if (validPropValue !== propValue) { + if (validRec === rec) { + validRec = { ...rec } + } + validRec[key] = validPropValue + } + } + + if (errors !== undefined && errors.length > 0) { + return failures(errors) + } + return success(validRec as any) + }, + useIdentity + ? identity + : (a) => { + const result: { [x: string]: any } = { ...a } + + for (let i = 0; i < required.count; ++i) { + const key = required.keys![i] + const encode = required.types![i].encode + if (encode !== identity) { + const propValue = result[key] + result[key] = encode(propValue) + } + } + + for (let i = 0; i < optional.count; ++i) { + const key = optional.keys![i] + const propValue = result[key] + const encode = optional.types![i].encode + if (propValue !== undefined && encode !== identity) { + result[key] = encode(propValue) + } + } + + return result as any + }, + properties + ) +} + /** * @since 1.0.0 */ diff --git a/test/2.1.x/semi-partial.ts b/test/2.1.x/semi-partial.ts new file mode 100644 index 000000000..739851517 --- /dev/null +++ b/test/2.1.x/semi-partial.ts @@ -0,0 +1,289 @@ +import * as assert from 'assert' +import { fold } from 'fp-ts/lib/Either' +import { pipe } from 'fp-ts/lib/pipeable' +import * as t from '../../src/index' +import { assertFailure, assertStrictEqual, assertSuccess, NumberFromString } from './helpers' + +describe('type', () => { + describe('name', () => { + it('should assign a default name', () => { + const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + assert.strictEqual(T.name, '{ a: string, b?: string }') + }) + + it('should accept a name', () => { + const T = t.type({ a: t.string }, 'T') + assert.strictEqual(T.name, 'T') + }) + }) + + describe('is', () => { + it('should return `true` on valid inputs', () => { + const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + assert.strictEqual(T.is({ a: 'a' }), true) + assert.strictEqual(T.is({ a: 'a', b: 'b' }), true) + }) + + it('should return `false` on invalid inputs', () => { + const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + assert.strictEqual(T.is({}), false) + assert.strictEqual(T.is({ a: 1 }), false) + assert.strictEqual(T.is({ b: 'b' }), false) + assert.strictEqual(T.is([]), false) + }) + + it('should return `false` on missing fields', () => { + const T = t.semiPartial({ a: t.unknown }) + assert.strictEqual(T.is({}), false) + }) + + it('should allow additional properties', () => { + const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + assert.strictEqual(T.is({ a: 'a', b: 'b', c: 'c' }), true) + }) + + it('should work for classes with getters', () => { + class A { + get a() { + return 'a' + } + get b() { + return 'b' + } + } + class B { + get a() { + return 'a' + } + } + class C { + get b() { + return 'b' + } + } + const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + assert.strictEqual(T.is(new A()), true) + assert.strictEqual(T.is(new B()), true) + assert.strictEqual(T.is(new C()), false) + }) + }) + + describe('decode', () => { + it('should decode a isomorphic value', () => { + const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + assertSuccess(T.decode({ a: 'a' })) + assertSuccess(T.decode({ a: 'a', b: 'b' })) + }) + + it('should decode a prismatic value', () => { + const T = t.semiPartial({ a: NumberFromString, b: { type: NumberFromString, optional: true } }) + assertSuccess(T.decode({ a: '1' }), { a: 1 }) + assertSuccess(T.decode({ a: '1', b: '2' }), { a: 1, b: 2 }) + }) + + it('should decode undefined properties as always present keys when required', () => { + const T1 = t.semiPartial({ a: t.undefined }) + assertSuccess(T1.decode({ a: undefined }), { a: undefined }) + assertSuccess(T1.decode({}), { a: undefined }) + + const T2 = t.semiPartial({ a: t.union([t.number, t.undefined]) }) + assertSuccess(T2.decode({ a: undefined }), { a: undefined }) + assertSuccess(T2.decode({ a: 1 }), { a: 1 }) + assertSuccess(T2.decode({}), { a: undefined }) + + const T3 = t.semiPartial({ a: t.unknown }) + assertSuccess(T3.decode({}), { a: undefined }) + }) + + it('should decode undefined properties as missing keys when optional and omitted', () => { + const T1 = t.semiPartial({ a: { type: t.undefined, optional: true } }) + assertSuccess(T1.decode({ a: undefined }), { a: undefined }) + assertSuccess(T1.decode({}), {}) + + const T2 = t.semiPartial({ a: { type: t.union([t.number, t.undefined]), optional: true } }) + assertSuccess(T2.decode({ a: undefined }), { a: undefined }) + assertSuccess(T2.decode({ a: 1 }), { a: 1 }) + assertSuccess(T2.decode({}), {}) + + const T3 = t.semiPartial({ a: { type: t.unknown, optional: true } }) + assertSuccess(T3.decode({}), {}) + }) + + it('should fail decoding an invalid value', () => { + const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + assertFailure(T, 1, ['Invalid value 1 supplied to : { a: string, b?: string }']) + assertFailure(T, {}, ['Invalid value undefined supplied to : { a: string, b?: string }/a: string']) + assertFailure(T, { a: 1 }, ['Invalid value 1 supplied to : { a: string, b?: string }/a: string']) + assertFailure(T, [], ['Invalid value [] supplied to : { a: string, b?: string }']) + }) + + it('#423', () => { + class A { + get a() { + return 'a' + } + get b() { + return 'b' + } + } + const T = t.semiPartial({ a: t.string, b: t.string }) + assertSuccess(T.decode(new A())) + }) + }) + + describe('encode', () => { + it('should encode a isomorphic value', () => { + const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + assert.deepStrictEqual(T.encode({ a: 'a' }), { a: 'a' }) + assert.deepStrictEqual(T.encode({ a: 'a', b: 'b' }), { a: 'a', b: 'b' }) + }) + + it('should encode a prismatic value', () => { + const T = t.semiPartial({ a: NumberFromString, b: { type: NumberFromString, optional: true } }) + assert.deepStrictEqual(T.encode({ a: 1 }), { a: '1' }) + assert.deepStrictEqual(T.encode({ a: 1, b: 2 }), { a: '1', b: '2' }) + }) + }) + + it('should keep unknown properties', () => { + const T = t.semiPartial({ a: t.string }) + const validation = T.decode({ a: 's', b: 1 }) + pipe( + validation, + fold( + () => { + assert.ok(false) + }, + (a) => { + assert.deepStrictEqual(a, { a: 's', b: 1 }) + } + ) + ) + }) + + it('should return the same reference if validation succeeded and nothing changed', () => { + const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + const value1 = { a: 's' } + assertStrictEqual(T.decode(value1), value1) + const value2 = { a: 's', b: 't' } + assertStrictEqual(T.decode(value2), value2) + }) + + it('should return the same reference while encoding', () => { + const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + assert.strictEqual(T.encode, t.identity) + }) + + it('should work for empty object', () => { + const T = t.semiPartial({}) + assert.deepStrictEqual(T.encode({}), {}) + assert.deepStrictEqual(T.decode({}), t.success({})) + assert.strictEqual(T.is({}), true) + }) + + it('should work for an object with all required properties', () => { + const type = t.semiPartial({ + p1: t.string, + p2: { + type: t.string + }, + p3: { + type: t.string, + optional: false + } + }) + const obj = { + p1: 'p1', + p2: 'p2', + p3: 'p3' + } + assert.deepStrictEqual(type.encode(obj), { + p1: 'p1', + p2: 'p2', + p3: 'p3' + }) + assert.deepStrictEqual( + type.decode(obj), + t.success({ + p1: 'p1', + p2: 'p2', + p3: 'p3' + }) + ) + assert.strictEqual(type.is(obj), true) + assert.strictEqual(type.is({}), false) + assert.strictEqual(type.is({ p1: 'p1' }), false) + }) + + it('should work for an object with all optional properties', () => { + const type = t.semiPartial( + { + p1: { + type: t.string, + optional: true + }, + p2: { + type: t.string, + optional: true + } + }, + 'Required' + ) + const obj = { + p1: 'p1', + p2: 'p2' + } + assert.deepStrictEqual(type.encode(obj), { + p1: 'p1', + p2: 'p2' + }) + assert.deepStrictEqual( + type.decode(obj), + t.success({ + p1: 'p1', + p2: 'p2' + }) + ) + assert.strictEqual(type.is(obj), true) + assert.strictEqual(type.is({}), true) + assert.strictEqual(type.is({ p1: 'p1' }), true) + }) + + it('should work for an object with a mix of optional and required props', () => { + const type = t.semiPartial( + { + p1: { + type: t.string, + optional: false + }, + p2: { + type: t.string, + optional: true + }, + p3: { + type: t.string, + optional: false + }, + p4: { + type: t.string, + optional: true + } + }, + 'Required' + ) + const obj = { + p1: 'p1', + p2: 'p2', + p3: 'p3', + p4: 'p4' + } + expect(type.encode(obj)).toEqual(obj) + expect(type.decode(obj)).toEqual(t.success(obj)) + assert.strictEqual(type.is(obj), true) + assert.strictEqual(type.is({}), false) + assert.strictEqual(type.is({ p1: 'p1', p2: 'p2' }), false) + assert.strictEqual(type.is({ p2: 'p2', p4: 'p4' }), false) + assert.strictEqual(type.is({ p1: 'p1', p3: 'p3' }), true) + assert.strictEqual(type.is({ p1: 'p1', p2: 'p2', p3: 'p3' }), true) + }) +}) From 276934f9a29e97c4fb929ce0b3b6556c889278f2 Mon Sep 17 00:00:00 2001 From: Matthew Burghoffer Date: Fri, 19 Aug 2022 09:54:20 -0700 Subject: [PATCH 2/3] refactoring partial, type, and semiPartial to share logic for is, validate, and encode --- src/index.ts | 325 +++++++++++++++++-------------------- test/2.1.x/helpers.ts | 7 + test/2.1.x/semi-partial.ts | 32 ++-- 3 files changed, 173 insertions(+), 191 deletions(-) diff --git a/src/index.ts b/src/index.ts index e32dce40b..3364839b0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1304,6 +1304,127 @@ export function array(item: C, name = `Array<${item.name}>`): A ) } +function testRequiredProps(value: { [key: string]: unknown }, count: number, keys: string[], types: Mixed[]): boolean { + for (let i = 0; i < count; ++i) { + const key = keys[i] + const propValue = value[key] + if (propValue === undefined && !hasOwnProperty.call(value, key)) { + return false + } + const type = types[i] + if (!type.is(propValue)) { + return false + } + } + return true +} + +function testOptionalProps(value: { [key: string]: unknown }, count: number, keys: string[], types: Mixed[]): boolean { + for (let i = 0; i < count; ++i) { + const key = keys[i] + const type = types[i] + const propValue = value[key] + if (propValue !== undefined && !type.is(propValue)) { + return false + } + } + return true +} + +function validateRequiredProps( + record: R, + count: number, + keys: string[], + types: Mixed[], + errors: Errors, + context: Context +): R { + let validRecord = record + for (let i = 0; i < count; ++i) { + const key = keys[i] + const type = types[i] + const propValue = record[key] + const result = type.validate(propValue, appendContext(context, key, type, propValue)) + if (isLeft(result)) { + pushAll(errors, result.left) + continue + } + const validPropValue = result.right + if (validPropValue !== propValue || (validPropValue === undefined && !hasOwnProperty.call(record, key))) { + if (validRecord === record) { + validRecord = { ...record } + } + validRecord[key as keyof R] = validPropValue + } + } + return validRecord +} + +function validateOptionalProps( + record: R, + count: number, + keys: string[], + types: Mixed[], + errors: Errors, + context: Context +): R { + let validRecord = record + for (let i = 0; i < count; ++i) { + const key = keys[i] + const propValue = record[key] + const type = types[i] + const result = type.validate(propValue, appendContext(context, key, type, propValue)) + if (isLeft(result)) { + if (propValue !== undefined) { + pushAll(errors, result.left) + } + } else { + const validPropValue = result.right + if (validPropValue !== propValue) { + if (validRecord === record) { + validRecord = { ...record } + } + validRecord[key as keyof R] = validPropValue + } + } + } + return validRecord +} + +function encodeRequiredProps( + record: R, + result: { [x: string]: any }, + count: number, + keys: string[], + types: Mixed[] +): void { + for (let i = 0; i < count; ++i) { + const key = keys[i] + const encode = types[i].encode + if (encode !== identity) { + const propValue = record[key] + result[key] = encode(propValue) + } + } +} + +function encodeOptionalProps( + record: R, + result: { [x: string]: any }, + count: number, + keys: string[], + types: Mixed[] +): void { + for (let i = 0; i < count; ++i) { + const key = keys[i] + const propValue = record[key] + const encode = types[i].encode + if (propValue !== undefined && encode !== identity) { + result[key] = encode(propValue) + } + } +} + /** * @since 1.0.0 */ @@ -1340,57 +1461,22 @@ export function type

(props: P, name: string = getInterfaceTypeN return new InterfaceType( name, (u): u is { [K in keyof P]: TypeOf } => { - if (UnknownRecord.is(u)) { - for (let i = 0; i < len; i++) { - const k = keys[i] - const uk = u[k] - if ((uk === undefined && !hasOwnProperty.call(u, k)) || !types[i].is(uk)) { - return false - } - } - return true - } - return false + return UnknownRecord.is(u) && testRequiredProps(u, len, keys, types) }, (u, c) => { const e = UnknownRecord.validate(u, c) if (isLeft(e)) { return e } - const o = e.right - let a = o const errors: Errors = [] - for (let i = 0; i < len; i++) { - const k = keys[i] - const ak = a[k] - const type = types[i] - const result = type.validate(ak, appendContext(c, k, type, ak)) - if (isLeft(result)) { - pushAll(errors, result.left) - } else { - const vak = result.right - if (vak !== ak || (vak === undefined && !hasOwnProperty.call(a, k))) { - /* istanbul ignore next */ - if (a === o) { - a = { ...o } - } - a[k] = vak - } - } - } + const a = validateRequiredProps(e.right, len, keys, types, errors, c) return errors.length > 0 ? failures(errors) : success(a as any) }, useIdentity(types) ? identity : (a) => { const s: { [x: string]: any } = { ...a } - for (let i = 0; i < len; i++) { - const k = keys[i] - const encode = types[i].encode - if (encode !== identity) { - s[k] = encode(a[k]) - } - } + encodeRequiredProps(a, s, len, keys, types) return s as any }, props @@ -1436,59 +1522,22 @@ export function partial

( return new PartialType( name, (u): u is { [K in keyof P]?: TypeOf } => { - if (UnknownRecord.is(u)) { - for (let i = 0; i < len; i++) { - const k = keys[i] - const uk = u[k] - if (uk !== undefined && !props[k].is(uk)) { - return false - } - } - return true - } - return false + return UnknownRecord.is(u) && testOptionalProps(u, len, keys, types) }, (u, c) => { const e = UnknownRecord.validate(u, c) if (isLeft(e)) { return e } - const o = e.right - let a = o const errors: Errors = [] - for (let i = 0; i < len; i++) { - const k = keys[i] - const ak = a[k] - const type = props[k] - const result = type.validate(ak, appendContext(c, k, type, ak)) - if (isLeft(result)) { - if (ak !== undefined) { - pushAll(errors, result.left) - } - } else { - const vak = result.right - if (vak !== ak) { - /* istanbul ignore next */ - if (a === o) { - a = { ...o } - } - a[k] = vak - } - } - } + const a = validateOptionalProps(e.right, len, keys, types, errors, c) return errors.length > 0 ? failures(errors) : success(a as any) }, useIdentity(types) ? identity : (a) => { const s: { [key: string]: any } = { ...a } - for (let i = 0; i < len; i++) { - const k = keys[i] - const ak = a[k] - if (ak !== undefined) { - s[k] = types[i].encode(ak) - } - } + encodeOptionalProps(a, s, len, keys, types) return s as any }, props @@ -1561,17 +1610,17 @@ export interface SemiPartialC

unknown > {} -interface PropsInfo

{ +interface SemiPropsFields

{ count: number keys?: StrKey

[] types?: AsType]>[] } -interface AllPropsInfo

{ +interface SemiPropsInfo

{ readonly name: string readonly useIdentity: boolean - readonly required: PropsInfo

- readonly optional: PropsInfo

+ readonly required: SemiPropsFields

+ readonly optional: SemiPropsFields

} function isTypedProp(prop: AnyProp): prop is TypedProp { @@ -1588,7 +1637,7 @@ function isTypedProp(prop: AnyProp): prop is TypedProp { } } -function getPartialPropsInfo

(properties: P, name: string | undefined): AllPropsInfo

{ +function getPartialPropsInfo

(properties: P, name: string | undefined): SemiPropsInfo

{ let requiredKeys: StrKey

[] | undefined let requiredTypes: AsType]>[] | undefined let requiredCount = 0 @@ -1707,117 +1756,35 @@ export function semiPartial

(properties: P, name?: string): return new SemiPartialType( typeName, - (u): u is SemiPartialC

=> { - // is it a record? + (u): u is { [K in keyof P]: TypeOf> } => { if (!UnknownRecord.is(u)) { return false } - - // test all the required properties - for (let i = 0; i < required.count; ++i) { - const key = required.keys![i] - const propValue = u[key] - if (propValue === undefined && !hasOwnProperty.call(u, key)) { - return false - } - const type = required.types![i] - if (!type.is(propValue)) { - return false - } + if (!testRequiredProps(u, required.count, required.keys!, required.types!)) { + return false } - - // test all the optional properties - for (let i = 0; i < optional.count; ++i) { - const key = optional.keys![i] - const type = optional.types![i] - const propValue = u[key] - if (propValue !== undefined && !type.is(propValue)) { - return false - } + if (!testOptionalProps(u, optional.count, optional.keys!, optional.types!)) { + return false } - return true }, (u, ctx) => { - // validate it is a record - const urv = UnknownRecord.validate(u, ctx) - if (isLeft(urv)) { - return urv + const e = UnknownRecord.validate(u, ctx) + if (isLeft(e)) { + return e } - - const rec = urv.right const errors: Errors = [] - let validRec = rec - - // validate all required properties - for (let i = 0; i < required.count; ++i) { - const key = required.keys![i] - const type = required.types![i] - const propValue = rec[key] - const result = type.validate(propValue, appendContext(ctx, key, type, propValue)) - if (isLeft(result)) { - pushAll(errors, result.left) - continue - } - const validPropValue = result.right - if (validPropValue !== propValue || (validPropValue === undefined && !hasOwnProperty.call(rec, key))) { - if (validRec === rec) { - validRec = { ...rec } - } - validRec[key] = validPropValue - } - } - - // validate all optional properties - for (let i = 0; i < optional.count; ++i) { - const key = optional.keys![i] - const propValue = rec[key] - if (propValue === undefined) { - continue - } - const type = optional.types![i] - const result = type.validate(propValue, appendContext(ctx, key, type, propValue)) - if (isLeft(result)) { - pushAll(errors, result.left) - continue - } - const validPropValue = result.right - if (validPropValue !== propValue) { - if (validRec === rec) { - validRec = { ...rec } - } - validRec[key] = validPropValue - } - } - - if (errors !== undefined && errors.length > 0) { - return failures(errors) - } - return success(validRec as any) + let a = e.right + a = validateRequiredProps(a, required.count, required.keys!, required.types!, errors, ctx) + a = validateOptionalProps(a, optional.count, optional.keys!, optional.types!, errors, ctx) + return errors.length > 0 ? failures(errors) : success(a as any) }, useIdentity ? identity : (a) => { const result: { [x: string]: any } = { ...a } - - for (let i = 0; i < required.count; ++i) { - const key = required.keys![i] - const encode = required.types![i].encode - if (encode !== identity) { - const propValue = result[key] - result[key] = encode(propValue) - } - } - - for (let i = 0; i < optional.count; ++i) { - const key = optional.keys![i] - const propValue = result[key] - const encode = optional.types![i].encode - if (propValue !== undefined && encode !== identity) { - result[key] = encode(propValue) - } - } - + encodeRequiredProps(a, result, required.count, required.keys!, required.types!) + encodeOptionalProps(a, result, optional.count, optional.keys!, optional.types!) return result as any }, properties diff --git a/test/2.1.x/helpers.ts b/test/2.1.x/helpers.ts index de7733520..03ca9b23c 100644 --- a/test/2.1.x/helpers.ts +++ b/test/2.1.x/helpers.ts @@ -109,3 +109,10 @@ export function withDefault( type.encode ) } + +export function asOptional(type: T): t.OptionalTypedProp> { + return { + type, + optional: true + } +} diff --git a/test/2.1.x/semi-partial.ts b/test/2.1.x/semi-partial.ts index 739851517..dc5805aff 100644 --- a/test/2.1.x/semi-partial.ts +++ b/test/2.1.x/semi-partial.ts @@ -2,12 +2,12 @@ import * as assert from 'assert' import { fold } from 'fp-ts/lib/Either' import { pipe } from 'fp-ts/lib/pipeable' import * as t from '../../src/index' -import { assertFailure, assertStrictEqual, assertSuccess, NumberFromString } from './helpers' +import { asOptional, assertFailure, assertStrictEqual, assertSuccess, NumberFromString, withDefault } from './helpers' describe('type', () => { describe('name', () => { it('should assign a default name', () => { - const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + const T = t.semiPartial({ a: t.string, b: asOptional(t.string) }) assert.strictEqual(T.name, '{ a: string, b?: string }') }) @@ -19,13 +19,13 @@ describe('type', () => { describe('is', () => { it('should return `true` on valid inputs', () => { - const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + const T = t.semiPartial({ a: t.string, b: asOptional(t.string) }) assert.strictEqual(T.is({ a: 'a' }), true) assert.strictEqual(T.is({ a: 'a', b: 'b' }), true) }) it('should return `false` on invalid inputs', () => { - const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + const T = t.semiPartial({ a: t.string, b: asOptional(t.string) }) assert.strictEqual(T.is({}), false) assert.strictEqual(T.is({ a: 1 }), false) assert.strictEqual(T.is({ b: 'b' }), false) @@ -38,7 +38,7 @@ describe('type', () => { }) it('should allow additional properties', () => { - const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + const T = t.semiPartial({ a: t.string, b: asOptional(t.string) }) assert.strictEqual(T.is({ a: 'a', b: 'b', c: 'c' }), true) }) @@ -61,7 +61,7 @@ describe('type', () => { return 'b' } } - const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + const T = t.semiPartial({ a: t.string, b: asOptional(t.string) }) assert.strictEqual(T.is(new A()), true) assert.strictEqual(T.is(new B()), true) assert.strictEqual(T.is(new C()), false) @@ -70,13 +70,13 @@ describe('type', () => { describe('decode', () => { it('should decode a isomorphic value', () => { - const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + const T = t.semiPartial({ a: t.string, b: asOptional(t.string) }) assertSuccess(T.decode({ a: 'a' })) assertSuccess(T.decode({ a: 'a', b: 'b' })) }) it('should decode a prismatic value', () => { - const T = t.semiPartial({ a: NumberFromString, b: { type: NumberFromString, optional: true } }) + const T = t.semiPartial({ a: NumberFromString, b: asOptional(NumberFromString) }) assertSuccess(T.decode({ a: '1' }), { a: 1 }) assertSuccess(T.decode({ a: '1', b: '2' }), { a: 1, b: 2 }) }) @@ -110,7 +110,7 @@ describe('type', () => { }) it('should fail decoding an invalid value', () => { - const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + const T = t.semiPartial({ a: t.string, b: asOptional(t.string) }) assertFailure(T, 1, ['Invalid value 1 supplied to : { a: string, b?: string }']) assertFailure(T, {}, ['Invalid value undefined supplied to : { a: string, b?: string }/a: string']) assertFailure(T, { a: 1 }, ['Invalid value 1 supplied to : { a: string, b?: string }/a: string']) @@ -129,11 +129,19 @@ describe('type', () => { const T = t.semiPartial({ a: t.string, b: t.string }) assertSuccess(T.decode(new A())) }) + + it('should support default values', () => { + const T = t.semiPartial({ + name: withDefault(t.string, 'foo') + }) + assertSuccess(T.decode({}), { name: 'foo' }) + assertSuccess(T.decode({ name: 'a' }), { name: 'a' }) + }) }) describe('encode', () => { it('should encode a isomorphic value', () => { - const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + const T = t.semiPartial({ a: t.string, b: asOptional(t.string) }) assert.deepStrictEqual(T.encode({ a: 'a' }), { a: 'a' }) assert.deepStrictEqual(T.encode({ a: 'a', b: 'b' }), { a: 'a', b: 'b' }) }) @@ -162,7 +170,7 @@ describe('type', () => { }) it('should return the same reference if validation succeeded and nothing changed', () => { - const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + const T = t.semiPartial({ a: t.string, b: asOptional(t.string) }) const value1 = { a: 's' } assertStrictEqual(T.decode(value1), value1) const value2 = { a: 's', b: 't' } @@ -170,7 +178,7 @@ describe('type', () => { }) it('should return the same reference while encoding', () => { - const T = t.semiPartial({ a: t.string, b: { type: t.string, optional: true } }) + const T = t.semiPartial({ a: t.string, b: asOptional(t.string) }) assert.strictEqual(T.encode, t.identity) }) From d2b316f4deea69d71cb8630209ccef85562d7543 Mon Sep 17 00:00:00 2001 From: Matthew Burghoffer Date: Fri, 19 Aug 2022 09:58:00 -0700 Subject: [PATCH 3/3] fixing lint issue --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 3364839b0..c8b5bb797 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1660,7 +1660,7 @@ function getPartialPropsInfo

(properties: P, name: string | let propType: AsType]> if (isTypedProp(prop)) { - propIsOptional = prop.optional ?? false + propIsOptional = prop.optional || false propType = prop.type as AsType]> } else { propIsOptional = false