Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Any thoughts on making my makeNullablePropsOptional function simpler to avoid dreaded "Type instantiation is excessively deep and possibly infinite."? #3905

Open
epatey opened this issue Dec 11, 2024 · 7 comments

Comments

@epatey
Copy link

epatey commented Dec 11, 2024

I have a helper function makeNullablePropsOptional that was inspired by #2050. I use it to turn null's coming from my database into undefined's within my app.

Unfortunately, after bumping zod recently, I started flirting with "Type instantiation is excessively deep and possibly infinite." errors. I'm relatively confident that use of this helper function correlates with the error.

Does anyone have any insight into how I might simplify the implementation in a way that might appease the TypeScript gods?

/**
 * This function takes a Zod schema and transforms all nullable properties into properties that use a transform to
 * handle null values.
 *
 * The implementation was inspired by https://github.com/colinhacks/zod/discussions/2050
 *
 * @param inputSchema - The input Zod schema object.
 * @returns A new Zod schema object with nullable properties transformed to handle null values.
 *
 * @example
 * const originalSchema = z.object({
 *   name: z.string(),
 *   age: z.number().nullable(),
 *   email: z.string().nullable(),
 * });
 * // type OriginalSchema = {
 * //   name: string;
 * //   age: number | null;
 * //   email: string | null;
 * // }
 *
 * const newSchema = makeNullablePropsOptional(originalSchema);
 *
 * // The newSchema will have the following shape:
 * // {
 * //   name: z.string(),
 * //   age: z.number().nullable().transform(x => x ?? undefined),
 * //   email: z.string().nullable().transform(x => x ?? undefined),
 * // }
 * // type NewSchema = {
 * //   name: string;
 * //   age: number | undefined;
 * //   email: string | undefined;
 * // }
 *
 */
export function makeNullablePropsOptional<TInputSchema extends z.AnyZodObject>(inputSchema: TInputSchema) {
  // Define a new type where each property in the schema's shape is transformed:
  // - If the property is a ZodNullable type, it is modified to transform `null` values to `undefined`.
  // - Otherwise, the property type remains unchanged.
  type OutputShape = {
    [key in keyof TInputSchema["shape"]]: TInputSchema["shape"][key] extends z.ZodNullable<infer T>
      ? z.ZodOptional<T>
      : TInputSchema["shape"][key]
  }

  // Unfortunately, I can't come up with a way to get the type of the value for a specific key for inside the reduce below.
  // This is because Object.entries() widens the type of object passed to:
  // `{ [s: string]: T; }`. This throws away specific key names and groups all value types into a single union type. e.g.
  // `[string, z.ZodString | z.ZodNullable<z.ZodNumber> | z.ZodNullable<z.ZodString>][]`
  // So, I'm forced to use `any`'s. ugh.
  type UnsafeOutputShape = { [key in keyof TInputSchema["shape"]]: z.ZodTypeAny }
  const inputShapeEntries = Object.entries(inputSchema.shape) as [keyof TInputSchema["shape"], z.ZodTypeAny][]

  const outputShape = inputShapeEntries.reduce((acc, [key, value]) => {
    // If the value is an instance of ZodNullable, transform it to handle null values.
    // Otherwise, keep the value unchanged.
    acc[key] = value instanceof z.ZodNullable ? value.transform(x => x ?? undefined) : value
    return acc
  }, {} as UnsafeOutputShape) as OutputShape
  // Return a new Zod object schema with the transformed properties.
  return z.object(outputShape)
}
@colinhacks
Copy link
Owner

colinhacks commented Dec 11, 2024

What version of Zod are you on? I can't reproduce on the latest version. If you don't have "strict" enabled tsconfig that can sometimes cause errors. Also having multiple versions of Zod installed simultaneously (try npm why zod or pnpm why zod).

@epatey
Copy link
Author

epatey commented Dec 11, 2024

Oh. Using this on a basic schema doesn't induce the error. I have a pretty hefty schema that merges a bunch of DAO schemas together and then does the null->optional stuff. That makes TS croak, but if I take off my helper, it's fine.

I'm in a monorepo, I use manypkg so all packages use the same version — which is 3.24.1.

The complexity of the schema is because I have a pretty complex SQL query that joins three tables. Here's what schema that matches the query results looks like.

const resultSchema = makeNullablePropsOptional(
  selectVesselSchema.omit({ scrapeVersion: true, updated: true, epfd: true }).merge(
    selectPositionSchema.omit({ mmsi: true, received: true, accuracy: true }).merge(
      selectVoyageSchema.omit({ mmsi: true, received: true, destination: true }).merge(
        z.object({
          status: navigationStatusSchema.nullable(),
          mostRecentPositionReceived: selectPositionSchema.shape.received,
          mostRecentVoyageReceived: selectVoyageSchema.shape.received.nullable(),
          destination: selectVoyageSchema.shape.destination.nullable(),
          month: selectVoyageSchema.shape.month.nullable(),
          day: selectVoyageSchema.shape.day.nullable(),
          hour: selectVoyageSchema.shape.hour.nullable(),
          minute: selectVoyageSchema.shape.minute.nullable(),
        }),
      ),
    ),
  ),
).array()

export type GetVesselsWithLatestPositionAndVoyageResult = z.infer<typeof resultSchema>

All of those selectXxx schemas come from drizzle-zod, so they may add some complexity. Regardless, when I get rid of my makeNullablePropsOptional, it doesn't induce the warning.

I should say that the error is occurring on a branch that bumps from 3.24.0 -> 3.24.1.

@colinhacks
Copy link
Owner

colinhacks commented Dec 11, 2024

There's virtually no changes between 3.24 and 3.24.1 that could cause this, so it seems probably this is due to multiple Zod versions in node_modules or a VS Code caching issue. If you haven't already, try wiping node_modules and running Reload Window.

You might also try making the return type of your utility instead of relying on that "inline" OutputShape type.

export function makeNullablePropsOptional<TInputSchema extends z.AnyZodObject>(inputSchema: TInputSchema): ZodObject<{
    [key in keyof TInputSchema["shape"]]: TInputSchema["shape"][key] extends z.ZodNullable<infer T>
      ? z.ZodOptional<T>
      : TInputSchema["shape"][key]
  }> {
  ...
}

@epatey
Copy link
Author

epatey commented Dec 12, 2024

Thanks for the insights. I'll keep messing around with it.

I'm certain that I have only a single zod version in my node_modules.

Do you think there's any chance that this change in 4e219d6 could have made any impact? I ask because I've tried again to toggle between 3.24.0 and 3.24.1 and I've confirmed that this alone induces the error.

@epatey
Copy link
Author

epatey commented Dec 12, 2024

@colinhacks, I did more digging. At least for me, this single change from 3.24.0 to 3.24.1's lib/types.d.ts causes the issue.
3.24.0

declare const objectType: <T extends ZodRawShape>(
  shape: T,
  params?: RawCreateParams
) => ZodObject<
  T,
  "strip",
  ZodTypeAny,
  {
    [k in keyof objectUtil.addQuestionMarks<
      baseObjectOutputType<T>,
      any
    >]: objectUtil.addQuestionMarks<baseObjectOutputType<T>, any>[k];
  },
  { [k_1 in keyof baseObjectInputType<T>]: baseObjectInputType<T>[k_1] }
>;

3.24.1

declare const objectType: <T extends ZodRawShape>(
  shape: T,
  params?: RawCreateParams
) => ZodObject<
  T,
  "strip",
  ZodTypeAny,
  objectOutputType<T, ZodTypeAny, "strip">,
  objectInputType<T, ZodTypeAny, "strip">
>;

If I overwrite the .1 version with the declaration from .0, the problem goes away.

I'm traveling the next couple of days, but I'll try to get a minimal repro for you.

@epatey
Copy link
Author

epatey commented Dec 13, 2024

@CyanoFresh
Copy link

yes, 3.24.1 is causing "Type instantiation is excessively deep and possibly infinite" in our setup.
3.24.0 works correctly.
Hopefully, it will be fixed quickly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants