colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
33.71k stars 1.17k forks source link

Generating Zod schema from TS type definitions #53

Closed danenania closed 2 years ago

danenania commented 4 years ago

Love this library! I'm just beginning to convert a large TS project with many types, and I'm wondering how feasible it would be to automatically run through a file and convert any TS types to a zod schema and the accompanying inferred type. Or perhaps using the language server somehow would be easier? Either way, it would remove a ton of tedium from adopting zod for an existing project.

I guess I'll just barrel through and convert them one by one, which will probably take me a few hours at least, but figured I'd suggest this for those who might end up in a similar spot.

PepCarmona commented 2 months ago

Just to add for completion, if your typescript declared interface has optional fields you will want to remove that optionality:

export type TypeToZod<T> = {
  [K in keyof T]-?: T[K] extends
    | Date
    | string
    | number
    | boolean
    | null
    | undefined
    ? undefined extends T[K]
      ? ZodOptional<ZodType<Exclude<T[K], undefined>>>
      : ZodType<T[K]>
    : ZodObject<TypeToZod<T[K]>>;
};

Might want to add Date in the mix?

[K in keyof T]: T[K] extends Date | boolean | number | string | null | undefined

An extension to @IliyanID comment above is this alternative approach that makes optional fields required with the z.ZodDefault<> type:

export type TypeToZod<T> = Required<{
    [K in keyof T]: T[K] extends string | number | boolean | null | undefined
        ? undefined extends T[K]
            ? z.ZodDefault<z.ZodType<Exclude<T[K], undefined>>>
            : z.ZodType<T[K]>
        : z.ZodObject<TypeToZod<T[K]>>;
}>;

export const createZodObject = <T>(obj: TypeToZod<T>) => {
    return z.object(obj);
};

This forces you to give a z.default() value to optional properties, so when you go to parse() the schema, you do not need to worry about undefined properties. For example: Given the type...

type Body = {
    prompt: string;
    size?: number;
};

I can create the schema...

const schema = createZodObject<Body>({
    prompt: z.string(),
    size: z.number().default(512),
});

Then do...

const { prompt, size } = schema.parse(body);

And size will be of type number and not undefined.

Harm-Nullix commented 2 months ago

I think what you mean is, if you would want to remove the optional fields, you can use this approach?

Because removing optional fields, in my view, feels a bit weird because they can be there. They should by typed optionally with their type or undefined, so T | undefined to say it simply.

Just to add for completion, if your typescript declared interface has optional fields you will want to remove that optionality:

export type TypeToZod<T> = {
  [K in keyof T]-?: T[K] extends
    | Date
    | string
    | number
    | boolean
    | null
    | undefined
    ? undefined extends T[K]
      ? ZodOptional<ZodType<Exclude<T[K], undefined>>>
      : ZodType<T[K]>
    : ZodObject<TypeToZod<T[K]>>;
};

Might want to add Date in the mix? [K in keyof T]: T[K] extends Date | boolean | number | string | null | undefined

An extension to @IliyanID comment above is this alternative approach that makes optional fields required with the z.ZodDefault<> type:

export type TypeToZod<T> = Required<{
    [K in keyof T]: T[K] extends string | number | boolean | null | undefined
        ? undefined extends T[K]
            ? z.ZodDefault<z.ZodType<Exclude<T[K], undefined>>>
            : z.ZodType<T[K]>
        : z.ZodObject<TypeToZod<T[K]>>;
}>;

export const createZodObject = <T>(obj: TypeToZod<T>) => {
    return z.object(obj);
};

This forces you to give a z.default() value to optional properties, so when you go to parse() the schema, you do not need to worry about undefined properties. For example: Given the type...

type Body = {
    prompt: string;
    size?: number;
};

I can create the schema...

const schema = createZodObject<Body>({
    prompt: z.string(),
    size: z.number().default(512),
});

Then do...

const { prompt, size } = schema.parse(body);

And size will be of type number and not undefined.