colinhacks / tozod

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support for Union Types

derekparsons718 opened this issue · comments

Union types appear not to work as expected.

Example:

interface A {
   a: number
}

interface B {
   b: string
}

type C = A | B;

const aSchema: toZod<A> = z.object({a: z.number()})
const bSchema: toZod<B> = z.object({b: z.string()})
const cSchema: toZod<C> = z.union([aSchema, bSchema]) //Error here

The above code causes this error:

Type 'ZodUnion<[ZodObject<{ a: ZodNumber; }, "passthrough", ZodTypeAny, A, A>, ZodObject<{ b: ZodString; }, "passthrough", ZodTypeAny, B, B>]>' is missing the following properties from type 'ZodObject<{ a: ZodNumber; } | { b: ZodString; }, "passthrough", ZodTypeAny, C, C>': _shape, _unknownKeys, _catchall, shape, and 15 more. ts(2740)

Is there a workaround for this? And is there any plan to support this kind of code in the future?

This workaround removes the error...

interface A {
   a: number
}

interface B {
   b: string
}

type C = A | B;

const aSchema: toZod<A> = z.object({a: z.number()})
const bSchema: toZod<B> = z.object({b: z.string()})
const cSchema: z.ZodUnion<[toZod<A>, toZod<B>]> = z.union([aSchema, bSchema]) //Note the new type of cSchema

... but the type of cSchema no longer references C at all, which means that cSchema could easily fall out of sync with C and cause validation problems. This is far from an ideal solution.

@colinhacks any chance you'll implement support for unions anytime soon?

+1

Tying in vain to get this working (or some variation of it)

type PipelineProvider = 'bitbucket' | 'github'

type AddPipelineData = {
    provider: PipelineProvider
    repositoryName: string
}

const bitbucket = z.literal('bitbucket')
const github = z.literal('github')

const addPipelineValidationSchema: toZod<AddPipelineData> = z.object({
    provider: z.union([bitbucket, github]),
    repositoryName: z.string().min(1, { message: 'Repository name is required' }),
})

EDIT: this is fixed in colinhacks/zod#1495 (comment)

I've been banging my head with this issue, the main problem I'm facing is how to support the f-king syntax of Zod that expects an Union<[A, ...A]>. In the meantime, I added a patch that turns "unions" into unions of any:

import * as z from 'zod';

declare type isAny<T> = [any extends T ? 'true' : 'false'] extends ['true'] ? true : false
declare type equals<X, Y> = [X] extends [Y] ? ([Y] extends [X] ? true : false) : false

export declare type toZodder<T> = 
    isAny<T> extends true ? z.ZodAny : 
    [T] extends [boolean] ? z.ZodBoolean : 
    [undefined] extends [T] ? 
        T extends undefined ? never : z.ZodOptional<toZodder<T>> : 
    [null] extends [T] ? 
        T extends null ? z.ZodNull : z.ZodNullable<toZodder<T>> : 
        T extends Array<infer U> ? z.ZodArray<toZodder<U>> : 
        T extends Promise<infer U> ? z.ZodPromise<toZodder<U>> : 
        equals<T, string> extends true ? z.ZodString : 
        equals<T, bigint> extends true ? z.ZodBigInt :
        equals<T, number> extends true ? z.ZodNumber :
        equals<T, Date> extends true ? z.ZodDate :
        T extends { [k: string]: any; } ? z.ZodObject<{ [k in keyof T]-?: toZodder<T[k]>; }, 'strip', z.ZodTypeAny, T, T> :
        (T extends any ? (k: T)=>void : never) extends ((k: infer I)=>void) ? z.ZodUnion<Readonly<[z.ZodTypeAny, ...z.ZodTypeAny[]]>>
    : z.ZodAny;

The line (T extends any ? (k: T)=>void : never) extends ((k: infer I)=>void) ? z.ZodUnion<Readonly<[z.ZodTypeAny, ...z.ZodTypeAny[]]>> is the Union to intersection logic added.

I am also needing support for unions.

For example. this should work:

type TestType = {
  type: "a" | "b";
};

export const TestSchema: toZod<TestType> = z.object({
  type: z.union([z.literal("a"), z.literal("b")]),
});

But it throws this error:

Type 'ZodObject<{ type: ZodUnion<[ZodLiteral<"a">, ZodLiteral<"b">]>; }, "strip", ZodTypeAny, { type: "a" | "b"; }, { type: "a" | "b"; }>' is not assignable to type 'ZodObject<{ type: never; }, "strip", ZodTypeAny, TestType, TestType>'.
  Type '{ type: z.ZodUnion<[z.ZodLiteral<"a">, z.ZodLiteral<"b">]>; }' is not assignable to type '{ type: never; }'.
    Types of property 'type' are incompatible.
      Type 'ZodUnion<[ZodLiteral<"a">, ZodLiteral<"b">]>' is not assignable to type 'never'.ts(2322)

Give this a try: colinhacks/zod#1495 (comment)

Using the new TypeScript satisfies works great, but it doesn't check for null, undefined, or optional properties (?).

type TestType = {
  foo?: "a" | "b";
};

export const TestSchema = z.object({
  foo: z.union([z.literal("a"), z.literal("b")]), // This should produce an error because "foo" is optional, but no error is shown
}) satisfies z.ZodType<TestType>;

So I tried combining toZod with satisfies:

export const TestSchema: toZod<TestType> = z.object({
  foo: z.union([z.literal("a"), z.literal("b")]).optional(),
}) satisfies z.ZodType<TestType>;

This should produce no errors because foo is optional and it has .optional() added, but it does produce this error:

Type 'ZodObject<{ foo: ZodOptional<ZodUnion<[ZodLiteral<"a">, ZodLiteral<"b">]>>; }, "strip", ZodTypeAny, { foo?: "a" | "b" | undefined; }, { ...; }>' is not assignable to type 'ZodObject<{ foo: ZodOptional<never> | ZodOptional<never>; }, "strip", ZodTypeAny, TestType, TestType>'.
  Type '{ foo: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"a">, z.ZodLiteral<"b">]>>; }' is not assignable to type '{ foo: ZodOptional<never> | ZodOptional<never>; }'.
    Types of property 'foo' are incompatible.
      Type 'ZodOptional<ZodUnion<[ZodLiteral<"a">, ZodLiteral<"b">]>>' is not assignable to type 'ZodOptional<never> | ZodOptional<never>'.ts(2322)

Here's the workaround I've been using. I just use a little Equals type helper to check if the original StringUnion type is identical to my Zod schema's type. This works for string unions, but you might need a more sophisticated implementation of Equals to support comparing more complex types.

// The type that we want to build a Zod schema for.
type StringUnion = 'cat' | 'dog' | 'bat' | 'frog';

// Our Zod schema, which we want to stay in sync with StringUnion
const myStringUnion = z.union([
  z.literal('cat'),
  z.literal('dog'),
  z.literal('bat'),
  z.literal('frog'),
]);
type MyStringUnion = z.infer<typeof myStringUnion>;

// Type helpers to check if two types are the same.
type Equals<T, U> = [T] extends [U] ? ([U] extends [T] ? 1 : 0) : 0;
type Expect<T extends 1> = T;

// We will get a type error here if the two types ever diverge.
type Check = Expect<Equals<StringUnion, MyStringUnion>>;