question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Generate union from options array

See original GitHub issue

Hello. 😀

How do I validate a Union with a large quantity?

const fruitsMap = [
  { uid: 1, name: "banana" },
  { uid: 2, name: "apple" },
  // ... many many many
] as const;

const arrowUIds = fruitsMap.map((v) => v.uid);
type FruitsUidUnions = typeof arrowUIds[number];

type Entity = {
  uid: FruitsUidUnions;

  // ... many many many
  foo: string;
  bar: string;
};

const schema: z.ZodSchema<Entity> = z.object({
  // It's a lot of work.
  uid: z.union([z.literal(1), z.literal(2)]),
});

const schema2: z.ZodSchema<Entity> = z.object({
  // Type Error
  uid: z.union(arrowUIds),
  foo: z.string(),
  bar: z.string()
});

const schema3: z.ZodSchema<Entity> = z.object({
  // Type Error
  uid: z.union(arrowUIds as [z.ZodAny, z.ZodAny, ...z.ZodAny[]]),
  foo: z.string(),
  bar: z.string()
});

const isValidUid = (p: unknown): p is FruitsUidUnions =>
  arrowUIds.some((v) => p === v);
const schema4: z.ZodSchema<Entity> = z.object({
  // How about something like this?
  uid: z.validate().refine(isValidUid),
  foo: z.string(),
  bar: z.string()
});

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:5 (1 by maintainers)

github_iconTop GitHub Comments

8reactions
fabien0102commented, Apr 25, 2021

Hi! Thanks for this very fun types challenge!

The problem is about arrays vs tuple, this is a solution to solve this:

const fruitsMap = [
    { uid: 1, name: "banana" },
    { uid: 2, name: "apple" },
    // ... many many many
  ] as const;

  const arrowUIds = fruitsMap.map((v) => v.uid);
  type FruitsUidUnions = typeof arrowUIds[number];

  // https://stackoverflow.com/questions/55127004/how-to-transform-union-type-to-tuple-type
  type UnionToIntersection<U> = (
    U extends any ? (k: U) => void : never
  ) extends (k: infer I) => void
    ? I
    : never;
  type LastOf<T> = UnionToIntersection<
    T extends any ? () => T : never
  > extends () => infer R
    ? R
    : never;

  // TS4.0+
  type Push<T extends any[], V> = [...T, z.ZodLiteral<V>]; // <- z.literal injected here

  // TS4.1+
  type TuplifyUnion<
    T,
    L = LastOf<T>,
    N = [T] extends [never] ? true : false
  > = true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>;

  const schema4 = z.object({
    // Victory!!! 🎉🎉
    uid: z.union(arrowUIds.map((id) => z.literal(id)) as TuplifyUnion<FruitsUidUnions>),
    foo: z.string(),
    bar: z.string(),
  });

This was fun to craft and should work! 😁

This said, my personnal solution for this kind of problem would be to generate the “lot of work” solution from a source.

As example, I have this script to extract z.enum from OpenAPI specs:

import { writeFileSync } from "fs";
import { isReferenceObject, OpenAPIObject } from "openapi3-ts";
import { join } from "path";
import { camel } from "case";

const extractEnums = (schema: OpenAPIObject): OpenAPIObject => {
  const outputPath = join(__dirname, "./hub/enums.ts");
  let fileBody = "";
  const typesToImport: string[] = [];
  Object.entries(schema.components!.schemas!).forEach(([component, def]) => {
    if (isReferenceObject(def)) return;
    if (def.enum && def.type === "string") {
      if (def.description) {
        fileBody += `/**\n * ${def.description}\n */\n`;
      }
      typesToImport.push(component);
      fileBody += `export const ${camel(component)}Enum = z.enum([${def.enum
        .map(i => `"${i}" as ${component}`)
        .join(", ")}]); \n\n`;
    }
  });

  const file = `/* Generated by restful-react */
  
  import { z } from "zod";
  import { ${typesToImport.join(", ")} } from "./hub";

  ${fileBody}
  `;

  writeFileSync(outputPath, file, "utf-8");
  return schema;
};

This should be way easier from a simple JSON (OpenAPI is a bit verbose 😅)

Or if you have some types, you can also use ts-to-zod (but no typeof support (yet))

I hope one of the solutions will help you solve your problem. Have fun!

6reactions
colinhackscommented, Apr 25, 2021

@fabien0102 that truly is some next-level TypeScript wizardry! 👏

I found a more abbreviated approach that probably makes a little more intuitive sense:

const fruitsMap = [
    { uid: 1, name: "banana" },
    { uid: 2, name: "apple" },
    // ... many many many
  ] as const;
  type fruitsMap = typeof fruitsMap;

  type toLiterals<T> = {
    -readonly [k in keyof T]: T[k] extends { uid: number }
      ? ZodLiteral<T[k]["uid"]>
      : never;
  };

  type fruitLiterals = toLiterals<fruitsMap>;
  const fruitLiterals: fruitLiterals = fruitsMap.map((fruit) =>
    z.literal(fruit.uid)
  ) as any;

  const schema4 = z.object({
    // Victory!!! 🎉🎉
    uid: z.union(fruitLiterals),
    foo: z.string(),
    bar: z.string(),
  });

Very tricky problem, took me a few tries to get this right. And I had to learn about the -readonly syntax which I’d never seen before. Good issue @baronTommy!

Read more comments on GitHub >

github_iconTop Results From Across the Web

String Union to string Array - typescript - Stack Overflow
Method for transforming string union into a non-duplicating array. Using keyof we can transform union into an array of keys of an object....
Read more >
2 ways to create a Union from an Array in Typescript
First way to create a union from an array​​ It turns out that using typeof keys[number] on any array will force Typescript to...
Read more >
Learn TypeScript: Union Types Cheatsheet - Codecademy
TypeScript lets you create a union type that is a composite of selected types ... TypeScript allows you to declare a union of...
Read more >
Handbook - Unions and Intersection Types - TypeScript
How to use unions and intersection types in TypeScript. ... for tools which let you compose or combine existing types instead of creating...
Read more >
Is it possible to create an array union types as values? - Reddit
If you want to enumerate over a union type, you're best off starting with the array and creating the type from that. This...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found