From 7dbabda131604b7be9d2fbb9b7649d137f8c0c3d Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Tue, 28 Dec 2021 13:43:02 -0500 Subject: [PATCH 1/2] WIP concat --- src/mixed.ts | 12 ++++-------- src/object.ts | 8 ++------ test/types/types.ts | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/mixed.ts b/src/mixed.ts index 4b2c31751..aac0e496a 100644 --- a/src/mixed.ts +++ b/src/mixed.ts @@ -1,5 +1,6 @@ -import { AnyObject, Maybe, Message, Optionals } from './types'; +import { AnyObject, Maybe, Message } from './types'; import type { + Concat, Defined, Flags, SetFlag, @@ -21,15 +22,10 @@ export declare class MixedSchema< concat( schema: MixedSchema, - ): MixedSchema | IT, TContext & IC, ID, TFlags | IF>; + ): MixedSchema, TContext & IC, ID, TFlags | IF>; concat( schema: BaseSchema, - ): MixedSchema< - NonNullable | Optionals, - TContext & IC, - ID, - TFlags | IF - >; + ): MixedSchema, TContext & IC, ID, TFlags | IF>; concat(schema: this): this; defined( diff --git a/src/object.ts b/src/object.ts index e7ab7f73c..81c5ba412 100644 --- a/src/object.ts +++ b/src/object.ts @@ -3,6 +3,7 @@ import { getter, normalizePath, join } from 'property-expr'; import { camelCase, snakeCase } from 'tiny-case'; import { + Concat, Flags, ISchema, SetFlag, @@ -311,12 +312,7 @@ export default class ObjectSchema< concat( schema: ObjectSchema, - ): ObjectSchema< - NonNullable | IIn, - TContext & IC, - TDefault & ID, - TFlags | IF - >; + ): ObjectSchema, TContext & IC, TDefault & ID, TFlags | IF>; concat(schema: this): this; concat(schema: any): any { let next = super.concat(schema) as any; diff --git a/test/types/types.ts b/test/types/types.ts index 2c3a5fe7c..35a85348b 100644 --- a/test/types/types.ts +++ b/test/types/types.ts @@ -622,6 +622,21 @@ Object: { // $ExpectType string merge.cast({}).other; + Concat: { + const obj1 = object({ + field: string().required(), + other: string().default(''), + }); + + const obj2 = object({ + field: number().default(1), + name: string(), + }).nullable(); + + // $ExpectType "foo" | null + obj1.concat(obj2).cast(''); + } + SchemaOfDate: { type Employee = { hire_date: Date; From 0bcc8fdfa597321b46c7b8ddc28f86affafef315 Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Tue, 28 Dec 2021 14:41:47 -0500 Subject: [PATCH 2/2] fixup object and docs --- README.md | 51 ++++++++++++++++++++++++++--------------- docs/typescript.md | 27 +++++----------------- src/object.ts | 25 +++++++++----------- src/util/objectTypes.ts | 9 ++++++++ test/types/types.ts | 11 ++++++++- 5 files changed, 68 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 9f8d6cda6..71ff949bd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Yup -Yup is a JavaScript schema builder for value parsing and validation. Define a schema, transform a value to match, validate the shape of an existing value, or both. Yup schema are extremely expressive and allow modeling complex, interdependent validations, or value transformations. +Yup is a JavaScript schema builder for value parsing and validation. Define a schema, transform a value to match, assert the shape of an existing value, or both. Yup schema are extremely expressive and allow modeling complex, interdependent validations, or value transformations. Yup's API is heavily inspired by [Joi](https://github.com/hapijs/joi), but leaner and built with client-side validation as its primary use-case. Yup separates the parsing and validating functions into separate steps. `cast()` transforms data while `validate` checks that the input is the correct shape. Each can be performed together (such as HTML form validation) or seperately (such as deserializing trusted data from APIs). @@ -38,20 +38,14 @@ let schema = yup.object().shape({ age: yup.number().required().positive().integer(), email: yup.string().email(), website: yup.string().url(), - createdOn: yup.date().default(function () { - return new Date(); - }), + createdOn: yup.date().default(() => new Date()), }); -// check validity -schema - .isValid({ - name: 'jimmy', - age: 24, - }) - .then(function (valid) { - valid; // => true - }); +// parse and assert validity +const parsedValue = await schema.validate({ + name: 'jimmy', + age: 24, +}); // you can try and type cast objects to the defined schema schema.cast({ @@ -59,6 +53,7 @@ schema.cast({ age: '24', createdOn: '2014-09-23T19:25:25Z', }); + // => { name: 'jimmy', age: 24, createdOn: Date } ``` @@ -77,8 +72,6 @@ import { } from 'yup'; ``` -> If you're looking for an easily serializable DSL for yup schema, check out [yup-ast](https://github.com/WASD-Team/yup-ast) - ### Using a custom locale dictionary Allows you to customize the default messages used by Yup, when no message is provided with a validation test. @@ -102,10 +95,12 @@ let schema = yup.object().shape({ age: yup.number().min(18), }); -schema.validate({ name: 'jimmy', age: 11 }).catch(function (err) { +try { + await schema.validate({ name: 'jimmy', age: 11 }); +} catch (err) { err.name; // => 'ValidationError' err.errors; // => ['Deve ser maior que 18'] -}); +} ``` If you need multi-language support, Yup has got you covered. The function `setLocale` accepts functions that can be used to generate error objects with translation keys and values. Just get this output and feed it into your favorite i18n library. @@ -131,10 +126,12 @@ let schema = yup.object().shape({ age: yup.number().min(18), }); -schema.validate({ name: 'jimmy', age: 11 }).catch(function (err) { +try { + await schema.validate({ name: 'jimmy', age: 11 }); +} catch (err) { err.name; // => 'ValidationError' err.errors; // => [{ key: 'field_too_short', values: { min: 18 } }] -}); +} ``` ## API @@ -385,6 +382,16 @@ SchemaDescription { #### `mixed.concat(schema: Schema): Schema` Creates a new instance of the schema by combining two schemas. Only schemas of the same type can be concatenated. +`concat` is not a "merge" function in the sense that all settings from the provided schema, override ones in the +base, including type, presence and nullability. + +```ts +mixed().defined().concat(mixed().nullable()); + +// produces the equivalent to: + +mixed().defined().nullable(); +``` #### `mixed.validate(value: any, options?: object): Promise` @@ -1159,6 +1166,12 @@ object({ }); ``` +#### `object.concat(schemaB: ObjectSchema): ObjectSchema` + +Creates a object schema, by applying all settings and fields from `schemaB` to the base, producing a new schema. +The object shape is shallowly merged with common fields from `schemaB` taking precedence over the base +fields. + #### `object.pick(keys: string[]): Schema` Create a new schema from a subset of the original's fields. diff --git a/docs/typescript.md b/docs/typescript.md index 4acc24d0a..ecac75f9a 100644 --- a/docs/typescript.md +++ b/docs/typescript.md @@ -1,22 +1,8 @@ ## TypeScript Support -`yup` comes with robust typescript support! However, because of how dynamic `yup` is -not everything can be statically typed safely, but for most cases it's "Good Enough". - -Note that `yup` schema actually produce _two_ different types: the result of casting an input, and the value after validation. -Why are these types different? Because a schema can produce a value via casting that -would not pass validation! - -```js -const schema = string().nullable().required(); - -schema.cast(null); // -> null -schema.validateSync(null); // ValidationError this is required! -``` - -By itself this seems weird, but has it's uses when handling user input. To get a -TypeScript type that matches all possible `cast()` values, use `yup.TypeOf`. -To produce a type that matches a valid object for the schema use `yup.Asserts>` +`yup` comes with robust typescript support, producing values and types from schema that +provide compile time type safety as well as runtime parsing and validation. Schema refine +their output as ```ts import * as yup from 'yup'; @@ -46,12 +32,11 @@ const personSchema = yup.object({ You can derive a type for the final validated object as follows: ```ts -import type { Asserts } from 'yup'; +import type { InferType } from 'yup'; -// you can also use a type alias by this displays better in tooling -interface Person extends Asserts {} +type Person = InferType; -const validated: Person = personSchema.validateSync(parsed); +const validated: Person = await personSchema.validate(value); ``` If you want the type produced by casting: diff --git a/src/object.ts b/src/object.ts index 81c5ba412..7e6648dbd 100644 --- a/src/object.ts +++ b/src/object.ts @@ -3,14 +3,12 @@ import { getter, normalizePath, join } from 'property-expr'; import { camelCase, snakeCase } from 'tiny-case'; import { - Concat, Flags, ISchema, SetFlag, ToggleDefault, UnsetFlag, } from './util/types'; - import { object as locale } from './locale'; import sortFields from './util/sortFields'; import sortByKeyOrder from './util/sortByKeyOrder'; @@ -23,6 +21,7 @@ import BaseSchema, { SchemaObjectDescription, SchemaSpec } from './schema'; import { ResolveOptions } from './Condition'; import type { AnyObject, + ConcatObjectTypes, DefaultFromShape, MakePartial, MergeObjectTypes, @@ -74,7 +73,7 @@ export default interface ObjectSchema< // important that this is `any` so that using `ObjectSchema`'s default // will match object schema regardless of defaults TDefault = any, - TFlags extends Flags = 'd', + TFlags extends Flags = '', > extends BaseSchema, TContext, TDefault, TFlags> { default>( def: Thunk, @@ -105,14 +104,14 @@ export default class ObjectSchema< TIn extends Maybe, TContext = AnyObject, TDefault = any, - TFlags extends Flags = 'd', + TFlags extends Flags = '', > extends BaseSchema, TContext, TDefault, TFlags> { fields: Shape, TContext> = Object.create(null); declare spec: ObjectSchemaSpec; private _sortErrors = defaultSort; - private _nodes: string[] = []; //readonly (keyof TIn & string)[] + private _nodes: string[] = []; private _excludedEdges: readonly [nodeA: string, nodeB: string][] = []; @@ -312,7 +311,12 @@ export default class ObjectSchema< concat( schema: ObjectSchema, - ): ObjectSchema, TContext & IC, TDefault & ID, TFlags | IF>; + ): ObjectSchema< + ConcatObjectTypes, + TContext & IC, + Extract extends never ? _> : ID, + TFlags | IF + >; concat(schema: this): this; concat(schema: any): any { let next = super.concat(schema) as any; @@ -320,14 +324,7 @@ export default class ObjectSchema< let nextFields = next.fields; for (let [field, schemaOrRef] of Object.entries(this.fields)) { const target = nextFields[field]; - if (target === undefined) { - nextFields[field] = schemaOrRef; - } else if ( - target instanceof BaseSchema && - schemaOrRef instanceof BaseSchema - ) { - nextFields[field] = schemaOrRef.concat(target); - } + nextFields[field] = target === undefined ? schemaOrRef : target; } return next.withMutation((s: any) => diff --git a/src/util/objectTypes.ts b/src/util/objectTypes.ts index b5686a1cf..ea976258f 100644 --- a/src/util/objectTypes.ts +++ b/src/util/objectTypes.ts @@ -20,6 +20,15 @@ export type MergeObjectTypes, U extends AnyObject> = | ({ [P in keyof T]: P extends keyof U ? U[P] : T[P] } & U) | Optionals; +export type ConcatObjectTypes< + T extends Maybe, + U extends Maybe, +> = + | ({ + [P in keyof T]: P extends keyof NonNullable ? NonNullable[P] : T[P]; + } & U) + | Optionals; + export type PartialDeep = T extends | string | number diff --git a/test/types/types.ts b/test/types/types.ts index 35a85348b..375944cb6 100644 --- a/test/types/types.ts +++ b/test/types/types.ts @@ -633,8 +633,17 @@ Object: { name: string(), }).nullable(); - // $ExpectType "foo" | null + // $ExpectType { name?: string | undefined; other: string; field: number; } | null obj1.concat(obj2).cast(''); + + // $ExpectType { name?: string | undefined; other: string; field: number; } + obj1.nullable().concat(obj2.nonNullable()).cast(''); + + // $ExpectType { field: 1; other: ""; name: undefined; } + obj1.nullable().concat(obj2.nonNullable()).getDefault(); + + // $ExpectType null + obj1.concat(obj2.default(null)).getDefault(); } SchemaOfDate: {