Repository: jquense/yup

Issue:
Improvement: Tuple should be better represented in the describe method
The `tuple` schema currently does not return any information about its inner types when using the `tuple([...]).describe()` method.

The schema currently maintains its inner types on a `types` property. This differs from the normal `array` schema that holds the state of its inner types on a property called `innerType`. I propose that we update the `tuple` schema to hold its state on the `innerType` as an array. And then we should export this in the describe method like an array schema.

See: https://github.com/jquense/yup/blob/master/src/tuple.ts

Renaming `types[]` to `innerTypes[]` on this schema is not required. But I'd like to make this information available in the describe method somehow. And I feel like just making `innerType` an array is the most familiar API. On an array the `innerType` will be a schema, on a tuple the `innerType` will be an array of schemas.

This is quite straight forward and I am happy to open a PR for it. But I wanted ask @jquense his opinion on it first.


Hints from issue comments:
@DaddyWarbucks: Closed via #1947 

Solution 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(