Repository: fabien0102/ts-to-zod
Pull request #257 (2024-07-22 20:26:50)

feat: handle rest operator in tuple type
# Why

Implements #256 


Patch:
diff --git a/example/heros.ts b/example/heros.ts
index fd78c43..972909d 100644
--- a/example/heros.ts
+++ b/example/heros.ts
@@ -24,6 +24,8 @@ export type SupermanEnemy = Superman["enemies"][-1];
 export type SupermanName = Superman["name"];
 export type SupermanInvinciblePower = Superman["powers"][2];
 
+export type PersonTuple = [Person, ...Person[]];
+
 export interface Superman {
   person: Person;
   name: "superman" | "clark kent" | "kal-l";
diff --git a/example/heros.types.ts b/example/heros.types.ts
index 44c8489..61f6b10 100644
--- a/example/heros.types.ts
+++ b/example/heros.types.ts
@@ -11,6 +11,14 @@ export type Enemy = z.infer<typeof generated.enemySchema>;
 
 export type Superman = z.infer<typeof generated.supermanSchema>;
 
+export type SupermanName = z.infer<typeof generated.supermanNameSchema>;
+
+export type SupermanInvinciblePower = z.infer<
+  typeof generated.supermanInvinciblePowerSchema
+>;
+
+export type PersonTuple = z.infer<typeof generated.personTupleSchema>;
+
 export type Villain = z.infer<typeof generated.villainSchema>;
 
 export type Story = z.infer<typeof generated.storySchema>;
@@ -29,12 +37,6 @@ export type HeroContact = z.infer<typeof generated.heroContactSchema>;
 
 export type SupermanEnemy = z.infer<typeof generated.supermanEnemySchema>;
 
-export type SupermanName = z.infer<typeof generated.supermanNameSchema>;
-
-export type SupermanInvinciblePower = z.infer<
-  typeof generated.supermanInvinciblePowerSchema
->;
-
 export type EvilPlan = z.infer<typeof generated.evilPlanSchema>;
 
 export type EvilPlanDetails = z.infer<typeof generated.evilPlanDetailsSchema>;
diff --git a/example/heros.zod.ts b/example/heros.zod.ts
index 46aa9f6..33d65f3 100644
--- a/example/heros.zod.ts
+++ b/example/heros.zod.ts
@@ -38,6 +38,13 @@ export const supermanSchema = z.object({
   ]),
 });
 
+export const supermanNameSchema = supermanSchema.shape.name;
+
+export const supermanInvinciblePowerSchema =
+  supermanSchema.shape.powers.items[2];
+
+export const personTupleSchema = z.tuple([personSchema]).rest(personSchema);
+
 export const villainSchema: z.ZodSchema<Villain> = z.lazy(() =>
   z.object({
     name: z.string(),
@@ -96,11 +103,6 @@ export const heroContactSchema = z.object({
 
 export const supermanEnemySchema = supermanSchema.shape.enemies.valueSchema;
 
-export const supermanNameSchema = supermanSchema.shape.name;
-
-export const supermanInvinciblePowerSchema =
-  supermanSchema.shape.powers.items[2];
-
 export const evilPlanSchema: z.ZodSchema<EvilPlan> = z.lazy(() =>
   z.object({
     owner: villainSchema,
diff --git a/src/core/generateZodSchema.ts b/src/core/generateZodSchema.ts
index 821b30d..556dcff 100644
--- a/src/core/generateZodSchema.ts
+++ b/src/core/generateZodSchema.ts
@@ -621,10 +621,60 @@ function buildZodPrimitive({
   }
 
   if (ts.isTupleTypeNode(typeNode)) {
-    const values = typeNode.elements.map((i) =>
+    // Handle last item separetely if it is a rest element
+    const lastItem = typeNode.elements[typeNode.elements.length - 1];
+    const restElement =
+      ts.isRestTypeNode(lastItem) && ts.isArrayTypeNode(lastItem.type)
+        ? lastItem.type.elementType
+        : undefined;
+
+    // Handle the rest element
+    if (restElement) {
+      const values = typeNode.elements
+        .slice(0, typeNode.elements.length - 1)
+        .map((node) =>
+          buildZodPrimitive({
+            z,
+            typeNode: ts.isNamedTupleMember(node) ? node.type : node,
+            isOptional: false,
+            jsDocTags: {},
+            sourceFile,
+            dependencies,
+            getDependencyName,
+            skipParseJSDoc,
+            customJSDocFormatTypes,
+          })
+        );
+
+      zodProperties.unshift({
+        identifier: "rest",
+        expressions: [
+          buildZodPrimitive({
+            z,
+            typeNode: restElement,
+            isOptional: false,
+            jsDocTags: {},
+            sourceFile,
+            dependencies,
+            getDependencyName,
+            skipParseJSDoc,
+            customJSDocFormatTypes,
+          }),
+        ],
+      });
+
+      return buildZodSchema(
+        z,
+        "tuple",
+        [f.createArrayLiteralExpression(values)],
+        zodProperties
+      );
+    }
+
+    const values = typeNode.elements.map((node) =>
       buildZodPrimitive({
         z,
-        typeNode: ts.isNamedTupleMember(i) ? i.type : i,
+        typeNode: ts.isNamedTupleMember(node) ? node.type : node,
         isOptional: false,
         jsDocTags: {},
         sourceFile,
diff --git a/src/utils/traverseTypes.ts b/src/utils/traverseTypes.ts
index c0fd8cd..853683d 100644
--- a/src/utils/traverseTypes.ts
+++ b/src/utils/traverseTypes.ts
@@ -61,6 +61,8 @@ export function getReferencedTypeNames(
       typeNode.forEachChild(visitorExtract);
     } else if (ts.isTupleTypeNode(typeNode)) {
       typeNode.elements.forEach(handleTypeNode);
+    } else if (ts.isRestTypeNode(typeNode)) {
+      handleTypeNode(typeNode.type);
     } else if (
       ts.isIntersectionTypeNode(typeNode) ||
       ts.isUnionTypeNode(typeNode)


Test patch:
diff --git a/src/core/generateZodSchema.test.ts b/src/core/generateZodSchema.test.ts
index 293897c..4a952f0 100644
--- a/src/core/generateZodSchema.test.ts
+++ b/src/core/generateZodSchema.test.ts
@@ -175,6 +175,13 @@ describe("generateZodSchema", () => {
     );
   });
 
+  it("should generate a tuple schema with rest operator", () => {
+    const source = `export type Life = [LoisLane, ...Problem[]];`;
+    expect(generate(source)).toMatchInlineSnapshot(
+      `"export const lifeSchema = z.tuple([loisLaneSchema]).rest(problemSchema);"`
+    );
+  });
+
   it("should generate an object schema", () => {
     const source = `export type Superman = {
      name: "superman";
diff --git a/src/utils/traverseTypes.test.ts b/src/utils/traverseTypes.test.ts
index d082f08..ba011a2 100644
--- a/src/utils/traverseTypes.test.ts
+++ b/src/utils/traverseTypes.test.ts
@@ -465,6 +465,18 @@ describe("traverseTypes", () => {
         { name: "Person", partOfQualifiedName: false },
       ]);
     });
+
+    it("should extract type when after a rest operator", () => {
+      const source = `
+        export type Hero = [Person, ...Skill[]]`;
+
+      const result = extractNames(source);
+      expect(result).toEqual([
+        { name: "Hero", partOfQualifiedName: false },
+        { name: "Person", partOfQualifiedName: false },
+        { name: "Skill", partOfQualifiedName: false },
+      ]);
+    });
   });
 });