Repository: jquense/yup
Pull request #1947 (2023-03-16 12:52:12)

Tuple describe() method
This PR addresses: https://github.com/jquense/yup/issues/1891

The code aims to standardize how Array and Tuple inner types are configured. Historically `array` has stored it's one inner type on a prop `innerType`. Tuple has stored its types in `spec.types`. The types MUST be stored on the `spec` object for Tuple because it is needed for the `notType` method. See: https://github.com/jquense/yup/blob/f37a53f45048eefe445c14b27b1b031583f9585d/src/locale.ts#L137

Both Array and Tuple now have `schema.innerType` and `schema.spec.innerType`. This was done for backwards compatibility and consistency. And all corresponding methods and types have been updated.

This now allows the Tuple to return information about its types in the `describe` method

```js
const description = tuple([string(), number()]).describe();

description = {
  type: 'tuple',
  innerType: [
    { type: 'string },
    { type: 'number' }
  ]
}
```

Let me know if you have any questions!

DaddyWarbucks: While I've got your eyes on this, this appears to be a bug in the array schema: https://github.com/jquense/yup/blob/b0e67ef173dbb04b1fbf0eb163b49d3bde1f3d6a/src/array.ts#L272
Shouldn't that pass `innerOptions`?
DaddyWarbucks: @jquense All updates have been made. let me know if you have anymore questions or concerns.

Patch:
diff --git a/src/array.ts b/src/array.ts
index c19f8ab..b03b74f 100644
--- a/src/array.ts
+++ b/src/array.ts
@@ -38,17 +38,23 @@ export function create<C extends Maybe<AnyObject> = AnyObject, T = any>(
   return new ArraySchema<T[] | undefined, C>(type as any);
 }
 
+interface ArraySchemaSpec<TIn, TContext> extends SchemaSpec<any> {
+  types?: ISchema<InnerType<TIn>, TContext>
+}
+
 export default class ArraySchema<
   TIn extends any[] | null | undefined,
   TContext,
   TDefault = undefined,
   TFlags extends Flags = '',
 > extends Schema<TIn, TContext, TDefault, TFlags> {
+  declare spec: ArraySchemaSpec<TIn, TContext>;
   readonly innerType?: ISchema<InnerType<TIn>, TContext>;
 
   constructor(type?: ISchema<InnerType<TIn>, TContext>) {
     super({
       type: 'array',
+      spec: { types: type } as ArraySchemaSpec<TIn, TContext>,
       check(v: any): v is NonNullable<TIn> {
         return Array.isArray(v);
       },
@@ -183,6 +189,11 @@ export default class ArraySchema<
     // @ts-expect-error readonly
     next.innerType = schema;
 
+    next.spec = {
+      ...next.spec,
+      types: schema as ISchema<InnerType<TIn>, TContext>
+    }
+
     return next as any;
   }
 
@@ -263,7 +274,7 @@ export default class ArraySchema<
           value: innerOptions.value[0],
         };
       }
-      base.innerType = this.innerType.describe(options);
+      base.innerType = this.innerType.describe(innerOptions);
     }
     return base;
   }
diff --git a/src/schema.ts b/src/schema.ts
index a1f46dc..363bec3 100644
--- a/src/schema.ts
+++ b/src/schema.ts
@@ -100,7 +100,7 @@ export interface SchemaRefDescription {
 }
 
 export interface SchemaInnerTypeDescription extends SchemaDescription {
-  innerType?: SchemaFieldDescription;
+  innerType?: SchemaFieldDescription | SchemaFieldDescription[];
 }
 
 export interface SchemaObjectDescription extends SchemaDescription {
diff --git a/src/tuple.ts b/src/tuple.ts
index 7cc796c..40fbf0c 100644
--- a/src/tuple.ts
+++ b/src/tuple.ts
@@ -11,7 +11,12 @@ import type {
   UnsetFlag,
   Maybe,
 } from './util/types';
-import Schema, { RunTest, SchemaSpec } from './schema';
+import type { ResolveOptions } from './Condition';
+import Schema, {
+  RunTest,
+  SchemaInnerTypeDescription,
+  SchemaSpec,
+} from './schema';
 import ValidationError from './ValidationError';
 import { tuple as tupleLocale } from './locale';
 
@@ -148,6 +153,22 @@ export default class TupleSchema<
       );
     });
   }
+
+  describe(options?: ResolveOptions<TContext>) {
+    let base = super.describe(options) as SchemaInnerTypeDescription;
+    base.innerType = this.spec.types.map((schema, index) => {
+      let innerOptions = options;
+      if (innerOptions?.value) {
+        innerOptions = {
+          ...innerOptions,
+          parent: innerOptions.value,
+          value: innerOptions.value[index],
+        };
+      }
+      return schema.describe(innerOptions);
+    });
+    return base;
+  }
 }
 
 create.prototype = TupleSchema.prototype;


Test patch:
diff --git a/test/mixed.ts b/test/mixed.ts
index 145e4cb..be55b59 100644
--- a/test/mixed.ts
+++ b/test/mixed.ts
@@ -9,6 +9,7 @@ import {
   ref,
   Schema,
   string,
+  tuple,
   ValidationError,
 } from '../src';
 import ObjectSchema from '../src/object';
@@ -965,6 +966,7 @@ describe('Mixed Types ', () => {
             is: 'entered',
             then: (s) => s.defined(),
           }),
+        baz: tuple([string(), number()])
       });
     });
 
@@ -976,7 +978,8 @@ describe('Mixed Types ', () => {
         default: {
           foo: undefined,
           bar: 'a',
-          lazy: undefined
+          lazy: undefined,
+          baz: undefined,
         },
         nullable: false,
         optional: true,
@@ -1035,6 +1038,41 @@ describe('Mixed Types ', () => {
               },
             ],
           },
+          baz: {
+            type: 'tuple',
+            meta: undefined,
+            label: undefined,
+            default: undefined,
+            nullable: false,
+            optional: true,
+            tests: [],
+            oneOf: [],
+            notOneOf: [],
+            innerType: [
+              {
+                type: 'string',
+                meta: undefined,
+                label: undefined,
+                default: undefined,
+                nullable: false,
+                optional: true,
+                oneOf: [],
+                notOneOf: [],
+                tests: [],
+              },
+              {
+                type: 'number',
+                meta: undefined,
+                label: undefined,
+                default: undefined,
+                nullable: false,
+                optional: true,
+                oneOf: [],
+                notOneOf: [],
+                tests: [],
+              }
+            ],
+          },
         },
       });
     });
@@ -1047,7 +1085,8 @@ describe('Mixed Types ', () => {
         default: {
           foo: undefined,
           bar: 'a',
-          lazy: undefined
+          lazy: undefined,
+          baz: undefined,
         },
         nullable: false,
         optional: true,
@@ -1111,6 +1150,41 @@ describe('Mixed Types ', () => {
               },
             ],
           },
+          baz: {
+            type: 'tuple',
+            meta: undefined,
+            label: undefined,
+            default: undefined,
+            nullable: false,
+            optional: true,
+            tests: [],
+            oneOf: [],
+            notOneOf: [],
+            innerType: [
+              {
+                type: 'string',
+                meta: undefined,
+                label: undefined,
+                default: undefined,
+                nullable: false,
+                optional: true,
+                oneOf: [],
+                notOneOf: [],
+                tests: [],
+              },
+              {
+                type: 'number',
+                meta: undefined,
+                label: undefined,
+                default: undefined,
+                nullable: false,
+                optional: true,
+                oneOf: [],
+                notOneOf: [],
+                tests: [],
+              }
+            ],
+          },
         },
       });
     });
diff --git a/test/tuple.ts b/test/tuple.ts
index 0fe576a..16bef54 100644
--- a/test/tuple.ts
+++ b/test/tuple.ts
@@ -91,7 +91,7 @@ describe('Array types', () => {
       );
     });
 
-    it('should throw useful type error for lenght', async () => {
+    it('should throw useful type error for length', async () => {
       let schema = tuple([string().label('name'), number().label('age')]);
 
       // expect(() => schema.cast(['James'])).toThrowError(