Skip to content

Inferred type from ZodUnion has odd effects when unioning class types #3962

Open
@CodeSmith32

Description

When using z.union inside an object with basic types like string, number, boolean, etc., the resulting type is pure, e.g.:

const zodType1 = z.object({
  subType: z.union([
    z.string(),
    z.number(),
    z.boolean(),
  ]),
});

type ComputedType1 = z.infer<typeof zodType1>;
// the computed type here is:  `{ subType: string | number | boolean }`

But as soon as an object type is added to the mix, the type becomes a strange intersection:

const zodType2 = z.object({
  subType: z.union([
    z.string(),
    z.number(),
    z.boolean(),
    z.date(),
  ]),
});

type ComputedType2 = z.infer<typeof zodType2>;
// the computed type here is:  `{ subType: (string | number | boolean | Date) & (string | number | boolean | Date | undefined) }`

 


 

This would normally never be an issue, but I'm attempting to use the following trick to make sure my zod types match types previously defined in pure TypeScript:

// from https://stackoverflow.com/questions/53807517/how-to-test-if-two-types-are-exactly-the-same
export type IfEquals<T, U, Y = unknown, N = never> =
  (<G>() => G extends T ? 1 : 2) extends <G>() => G extends U ? 1 : 2 ? Y : N;

export type AssertType<T, U> = IfEquals<T, U, string, false>;

// usage of AssertType
"assertion name" as AssertType<{a: true}, {a: true}>;

// error: Conversion of type 'string' to type 'false' may be a mistake...
"assertion name" as AssertType<string, number>;

This means that these won't match:

const zodType = z.object({
  subType: z.union([
    z.string(),
    z.number(),
    z.boolean(),
    z.date(),
  ]),
});

interface Type {
  subType: string | number | boolean | Date;
}

// gives an error
"zodType" as AssertType<z.infer<typeof zodType>, Type>;

In this case, it can be fixed by updating the type,

interface TypeFix {
  subType: (string | number | boolean | Date)
    & (string | number | boolean | Date | undefined);
}

// no error
"zodType" as AssertType<z.infer<typeof zodType>, TypeFix>;

But when this union is with an object, it never seems to match:

const objectType = z.object({
  x: z.number(),
  y: z.number(),
});

const zodType = z.object({
  subType: z.union([
    z.string(),
    z.number(),
    z.boolean(),
    objectType,
  ]),
});

interface ObjectType {
  x: number,
  y: number,
}

interface Type {
  subType: string | number | boolean | ObjectType;
}

interface TypeFix {
  subType: (string | number | boolean | ObjectType)
    & (string | number | boolean | ObjectType | undefined);
}

// gives an error
"zodType" as AssertType<z.infer<typeof zodType>, Type>;

// *still* gives an error (I have no idea why)
"zodType" as AssertType<z.infer<typeof zodType>, TypeFix>;

  1. Is there an alternative way of comparing types that would work? I've tried a number of other comparison tricks, but adding an optional property to the zod type isn't caught by any comparators except the one above.
  2. Is there a way to force-specify the type that Zod will output? e.g., z.ZodUnion spits out this odd type, but if I could force it to spit out the correct union type, the error wouldn't occur.
  3. Perhaps this is a TypeScript error, and not an issue with Zod? I'm not sure how to identify the underlying issue well enough to post it there though..

I understand that common practice is to specify the types as Zod types, and pull the TypeScript types from them. Unfortunately, right now, our project won't let us do this, just in its setup.

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions