Closed derekparsons718 closed 3 years ago
Just to add some ideas, I'm actually using the following pattern to compare zod & typescript
import { z } from "zod"
function expectType<T>(_: T) {
/* noop */
}
type Something = string;
const somethingSchema = z.string();
type SomethingFromZod = z.infer<typeof somethingSchema>;
expectType<Something>({} as SomethingFromZod);
expectType<SomethingFromZod>({} as Something); // Please note that we need to try both directions!
I tried tsd
Β and the following helper before:
export type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false
but had some problem with Record<string, any>
vs {[key: string]: any}
comparison
At the end this simple empty function was the most reliable (I tried on 500ish type definitions to find edge cases on my ts-to-zod generator)
But, I do agree, a little toZod<A>
sounds amazing to avoid this ugly checking π
In regard to the API, I don't think we need a new toZod
, if possible, we should just improve the safety of z.ZodSchema<>
@fabien0102 Your workaround is a great idea! I hadn't considered doing anything like that and it is good to know such a workaround is available. It is not ideal, but certainly better than nothing! I wonder how feasible it is to implement something like this in large application? I'm excited to try it out.
I love your second idea of improving the ZodSchema
type as well. I agree that it is probably a better alternative than a new tozod
.
@derekparsons718 For a large application, my current plan is the following:
z.infer<>
to have only one source of truthI'm currently working on adding a generated zod middleware on my fetchers, but works a bit to well, I'm waiting for some fixes on my backend π
So far this is for the frontend part, and utilized restful-react / open-api for the types generation, but I'm working on a more agnostic solution with a friend, still around open-api to generate some type definitions but not dependant to restful-react (even if you can already use restful-react just to generate the types (https://github.com/contiamo/restful-react#only-generating-custom-code-no-react-hookscomponents))
To summarized, two choices for me: zod + z.infer<>
or fully generated to have types & zod in sync
I hope this can help you π
EDIT: I recommend a more rigorous solution here: https://github.com/colinhacks/zod/issues/372#issuecomment-2445439772
I recommend using a utility function like this:
const schemaForType = <T>() => <S extends z.ZodType<T, any, any>>(arg: S) => {
return arg;
};
// use like this:
const dog = schemaForType<Dog>()(
z.object({
name: z.string(),
neutered: z.boolean(),
})
);
The nested functions may seem weird, but it's necessary because there are two levels of generics required: the desired TypeScript type T
and the inferred schema S
which is constrained by extends z.ZodType<T, any, any>
. Don't worry about what the other two any
s are. Since we're casting providing T
as a type hint directly, we need a separate function that lets us infer the type of S. This is because TypeScript requires you to either explicitly specify all generic parameters or let them all get inferred. You can't mix and match, though there's a proposal to support that here: https://github.com/microsoft/TypeScript/issues/26242
This helper returns the schema as-is and the typechecker makes sure schema S
validates the type T
. CC @fabien0102 @derekparsons718
If you're using Zod for API validation, I'd recommend looking into tRPC: https://trpc.io. No codegen, and it's easier than going through OpenAPI as an intermediate representation. Plus if you set up a monorepo right (or if you use Next.js) you can share your Zod schemas between client and server which is a huge win. CC @fabien
@colinhacks Wow, that utility function seems to work perfectly! (Typescript was not happy about that second any
, saying that ZodType
requires 1 or 2 arguments instead of 3, but I just removed that last any
and it seems to work fine.) Based on the quick experiments I did, this is exactly what I am looking for! It's beautiful.
Unfortunately I have moved to other projects where I can't easily implement this in a large application for a while. But I don't see why it wouldn't work.
Is it feasible to add this utility function to the official zod package? That would be the icing on the cake!
(Only tested in Zod3)
In case this helps someone else, for simple cases I've also found it useful to work with shapes to describe existing types that are to be used with z.object()
.
This makes autocompletion work as expected (keys from your type are suggested for your schema), and I've found it convenient to deal with simple object merges: z.object(...shape1, ...shape2)
.
Given
type Dog = {
name: string;
neutered: boolean;
remarks?: string;
};
and a utility that conforms to ZodRawShape
:
type ZodShape<T> = {
// Require all the keys from T
[key in keyof T]-?: undefined extends T[key]
? // When optional, require the type to be optional in zod
z.ZodOptionalType<z.ZodType<T[key]>>
: z.ZodType<T[key]>;
};
You can typecheck against existing types in the following way:
const dogShape: ZodShape<Dog> = {
name: z.string(),
neutered: z.boolean(),
remarks: z.string().optional(),
};
const dogSchema = z.object(dogShape);
Then if you care about dealing with the original shape, you can use the trick from https://github.com/colinhacks/zod/issues/372#issuecomment-826380330, but with shapes:
const shapeForType = <T>() => <S extends ZodShape<T>>(arg: S) => {
return arg;
};
...
const dogShape = shapeForType<Dog>()({
name: z.string(),
neutered: z.boolean(),
remarks: z.string().optional()
});
const dogSchema = z.object(dogShape);
That way you'd still have autocompletion and the distinction between optional and required keys.
@ridem that's basically a less complete version of the toZod
utility mentioned earlier on this thread, which tries to map a typescript type to a Zod schema type: https://github.com/colinhacks/tozod/blob/master/src/index.ts
It's not ideal because to lose internal type information for each element of the ZodObject (since everything gets casted to ZodType):
But if it's good enough for your needs, then by all means use that approach! Nothing inherently wrong with it, it just has limitations.
Yes, no matter how fancy the conversion utility needs to be, thinking in terms of shapes has been the most efficient way for us to implement zod with existing types, so I just wanted to share this on this thread too. What I shared is definitely not a way to convert types to zod, just a way to go about checking they're in sync and writing zod.
@colinhacks Just out of curiosity, what info is lost in 'shapeForType' compared to what you suggested in https://github.com/colinhacks/zod/issues/372#issuecomment-826380330 ?
type Dog = {
name: string;
neutered: boolean;
};
const myObject: toZod<Dog> = z.object({
name: z.string(),
neutered: z.boolean()
});
type ZodShape<T> = {
// Require all the keys from T
[key in keyof T]-?: undefined extends T[key]
? // When optional, require the type to be optional in zod
z.ZodOptionalType<z.ZodType<T[key]>>
: z.ZodType<T[key]>;
};
const yourShape: ZodShape<Dog> = {
name: z.string(),
neutered: z.boolean(),
}
;
const yourObject = z.object(yourShape)
myObject.shape.name; // ZodString
myObject.shape.name.max(5) // has string methods
yourObject.shape.name; // ZodType
// can't use ZodString-specific methods without
// TypeScript getting mad
Typescript was not happy about that second any, saying that ZodType requires 1 or 2 arguments instead of 3, but I just removed that last any and it seems to work fine
@derekparsons718 Oh yeah, that code assumes Zod 3 (which has three parameters on ZodType). Good call.
@colinhacks any way to get this to work? Tried ZodSchema
and its throwing a number of errors :S
@mmahalwy This is the recommended approach : https://github.com/colinhacks/zod/issues/372#issuecomment-826380330
For anyone looking to validate the input schema against an existing type, here's the small tweak I made to https://github.com/colinhacks/zod/issues/372#issuecomment-826380330 that works great:
const inputSchemaForType =
<TInput>() => <S extends z.ZodType<any, any, TInput>>(arg: S) => {
return arg;
};
// Then test it against an input schema
const InputSchema = z.string().transform((value) => Number.parseInt(value, 10));
// Validates
inputSchemaForType<string>()(InputSchema);
Is there a way to make https://github.com/colinhacks/zod/issues/372#issuecomment-830713601 require optional keys also be in the schema (as optional)?
@colinhacks It seems the recommended approach https://github.com/colinhacks/zod/issues/372#issuecomment-826380330 allows additional object properties:
const schemaForType = <T>() => <S extends z.ZodType<T, any, any>>(arg: S) => {
return arg;
};
const extra = schemaForType<{foo:string}>()(
z.object({
foo:z.string(),
bar: z.number(),
})
);
@colinhacks was there a reason why this wouldn't work in place of nested functions?
const schemaForType = <Type, Schema = z.ZodType<Type, SAFE_ANY, SAFE_ANY>>(schema: Schema) => {
return schema;
};
EDIT:
Usage example
export interface Folder {
type: 'Folder';
id: number;
createdAt: string;
ownerId?: string;
name: string;
}
export const folderSchema = schemaForType<Folder>(
z.object({
type: z.literal('Folder').default('Folder'),
id: z.number(),
createdAt: z.string(),
ownerId: z.string().optional(),
name: z.string(),
})
);
@colinhacks It seems the recommended approach #372 (comment) allows additional object properties:
Ah yeah, this is a pretty important point!
Maybe it would be possible with a utility like Exact
from ts-toolbelt
- preventing the extra properties... π€
For now, because there is no robust solution to use existing types built in to Zod, I'm staying away from the library. Don't want to rewrite all of our TS types in a non-standard, proprietary syntax - high risk if Zod becomes unmaintained or for some other reason becomes a bad choice in the future.
But if there was a robust, built-in solution for validating schemas against existing TS types, would love to give Zod another look!!
@colinhacks I'm in love with Zod, the biggest issue I'm facing right now is validating objects against Prisma types.
Mainly the fact that Prisma types return nullable but my API needs to allow for optional. Is there any way of letting Zod know it can be optional or transforming the value?
type Dog = {
name: string;
remark: boolean | null;
};
const dogPrismaSchema: toZod<Dog> = z.object({
name: z.string(),
// I need this to be optional without throwing an error.
remark: z.boolean().nullable(),
});
@TechnoPvP how about z.boolean().optional();
Thanks to @ridem I made a version that error if a schema property doesn't exist in the type to implement.
.nullish()
or .optional().nullable()
or just .nullable()
/ .optional()
.type Implements<Model> = {
[key in keyof Model]-?: undefined extends Model[key]
? null extends Model[key]
? z.ZodNullableType<z.ZodOptionalType<z.ZodType<Model[key]>>>
: z.ZodOptionalType<z.ZodType<Model[key]>>
: null extends Model[key]
? z.ZodNullableType<z.ZodType<Model[key]>>
: z.ZodType<Model[key]>;
};
export function implement<Model = never>() {
return {
with: <
Schema extends Implements<Model> & {
[unknownKey in Exclude<keyof Schema, keyof Model>]: never;
}
>(
schema: Schema
) => z.object(schema),
};
}
// usage
export type UserModel = {
id: string
phoneNumber: string
email: string | null
name: string
firstName: string
companyName: string
avatarURL: string
createdAt: Date
updatedAt: Date
}
export const UserModelSchema = implement<UserModel>().with({
id: z.string(),
phoneNumber: z.string(),
email: z.string().email().nullable(),
name: z.string(),
firstName: z.string(),
avatarURL: z.string(),
companyName: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
});
All types seems preserved (even unions made with z.enum(["val1", "val2"] as const)
).
@karlhorky give it a try to that ;) I use this with Prisma model, I'm now confident about my types
@colinhacks what do you think of @rphlmr's approach above?
Do you think this could be integrated into Zod for first-class type comparison support?
The only thing I can't type is .nullable().optional()
π€·ββοΈ It must be .optional().nullable()
or .nullish()
.
Not a big deal I think.
@rphlmr This is exactly what I was looking for. Nice job!
@rphlmr great solution, thank you!!!!!
I'm quite new to Zod and I am trying to cover the discriminated unions too, but I cannot come to a solution. For instance, starting from your User
type
type User = {
id: number;
name?: string;
dob: Date;
color: 'red' | 'blue';
};
type HawaiianPizzaUser = User &
(
| {
country: 'italy';
dontLikeHawaiianPizza: true;
}
| {
country: 'us';
likeHawaiianPizza: true;
}
);
export const hawaiianPizzaUserSchema = implement<HawaiianPizzaUser>().with(
{
// ... ???
}
);
can you think of anything?
@rphlmr your exact example doesn't work for me somehow.. It gives the error:
Property 'unwrap' is missing in type 'ZodString' but required in type 'ZodNullable<ZodOptionalType<ZodType<string, ZodTypeDef, string>>>'.ts(2741)
types.d.ts(654, 5): 'unwrap' is declared here.
test.ts(26, 3): The expected type comes from property 'id' which is declared here on type 'Implements<UserModel> & {}'
All attributes of UserModelSchema give this error. I'm using zod 3.19.1. Any thoughts?
EDIT
Found it! I needed to set "strictNullChecks": true,
in my tsconfig.json
. thanks @rphlmr for this nice little tool!
@rphlmr I'm getting a weird behavior when using z.enum(['a', 'b'] as const).default('a')
in my zod schema:
Type 'undefined' is not assignable to type '"a" | "b"'.
Any ideas about what might I be missing? :/
@ldiego08 I think it's because when you set a .default
, It guesses that the field can be nullable (undefined or null).
This works π
const Schema = implement<{ enum?: "a" | "b" }>().with({
enum: z
.enum(["a", "b"] as const)
.default("a")
.optional(),
});
For the other way ( from zod schema to typescript) i'm using the following code:
const PostData = z.object({
title: z.string(),
description: z.string(),
authorID: z.string(),
keywords: z.array(z.string()),
tags: z.array(z.string()),
lang: z.string(),
draft: z.boolean(),
});
export type PostType = Required<typeof PostData._type>;
const Schema = implement<{ enum?: "a" | "b" }>().with({ enum: z .enum(["a", "b"] as const) .default("a") .optional(), });
@rphlmr - I am very new to TS; in this above example, when I remove b
, it does not error. Any way to fix that?
const Schema = implement<{ enum?: "a" | "b" }>().with({
enum: z
.enum(["a"] as const)
.default("a")
.optional(),
});
@kevbook I guess it's because enum?
is optional. So, z.enum(xx).optional()
requires at least one of the possible values but doesn't error if missing one (it still throws an error if it's not a possible value of enum?: "a" | "b"
).
I don't know how to enforce it to require all values in the schema, sorry π₯²
I think it's also possible to update the solution in https://github.com/colinhacks/zod/issues/372#issuecomment-826380330 to prevent additional object properties by requiring that any keys of the inferred type of S are also keys of T:
export const schemaForType =
<T>() =>
<
S extends z.ZodType<T, any, any> &
z.ZodType<
{
[K in keyof z.infer<S>]: K extends keyof T ? z.infer<S>[K] : never;
},
any,
any
>,
>(
arg: S,
): S => {
return arg;
};
const extra = schemaForType<{foo:string}>()(
z.object({
foo: z.string(),
bar: z.number(),
})
);
With error message:
The types of '_type.bar' are incompatible between these types.
Type 'number' is not assignable to type 'never'.ts(2345)
As for the original question, I recommend using a utility function like this:
const schemaForType = <T>() => <S extends z.ZodType<T, any, any>>(arg: S) => { return arg; }; // use like this: const dog = schemaForType<Dog>()( z.object({ name: z.string(), neutered: z.boolean(), }) );
for anyone else finding this, it can now be done super cleanly in ts 4.9 with satisfies
// use like this:
const dog = z.object({
name: z.string(),
neutered: z.boolean(),
}) satisifes z.ZodType<Dog, any, any>;
it can now be done super cleanly in ts 4.9 with satisfies
Oh wow, this satisfies
example looks great! Very simple and readable.
@colinhacks should this be the official, documented solution for this problem? Are there any downsides to this vs. @rphlmr's approach from his comment above?
@rphlmr also posted a discussion about getting his approach added as a helper to Zod, but it has not received any feedback.
it can now be done super cleanly in ts 4.9 with satisfies
Oh wow, this
satisfies
example looks great! Very simple and readable.@colinhacks should this be the official, documented solution for this problem? Are there any downsides to this vs. @rphlmr's approach from his comment above?
@rphlmr also posted a discussion about getting his approach added as a helper to Zod, but it has not received any feedback.
I have tonnes of schemas in my app, will give a feedback tomorrow π€©
hi
I think using satisfies
is not enough
Here is the result I got
import { z } from 'zod';
type User = {
id: number
name: string
// age: number
}
const UserSchema = z.object({
id: z.number(),
name: z.string(),
age: z.number() // <- should be an error
}) satisfies z.ZodType<User>
const aa ={
name: 'hello',
id: 1,
age: 3, // causes an error
} satisfies User
Here is the typescript playground
So I think @rphlmr's approach from https://github.com/colinhacks/zod/issues/372#issuecomment-1280054492 works fine overall
@stijnjanmaat, I'm getting a similar 'unwrap' error that you were getting when I try to use toZod. Any idea how to get rid of this error?
I'm trying to do this:
const TreeSchema: toZod<ReturnTypeTree> = z.object({...schema properties go here})
But I'm getting this error:
Property 'unwrap' is missing in type 'ZodObject<{}, "strip", ZodTypeAny, {}, {}>' but required in type 'ZodOptional<ZodArray<ZodObject<{ product: ZodNullable<ZodObject<{ id: ZodString...etc
@rphlmr
Doesn't seem to work properly for nested types. Any ideas on how to fix this?
import { z } from "zod";
type Implements<Model> = {
[key in keyof Model]-?: undefined extends Model[key]
? null extends Model[key]
? z.ZodNullableType<z.ZodOptionalType<z.ZodType<Model[key]>>>
: z.ZodOptionalType<z.ZodType<Model[key]>>
: null extends Model[key]
? z.ZodNullableType<z.ZodType<Model[key]>>
: z.ZodType<Model[key]>;
};
export function implement<Model = never>() {
return {
with: <
Schema extends Implements<Model> & {
[unknownKey in Exclude<keyof Schema, keyof Model>]: never;
}
>(
schema: Schema
) => z.object(schema),
};
}
// usage
type InterfacePurpose = "public" | "vlan";
interface Interface {
label: string | null;
purpose: InterfacePurpose;
}
interface UserData {
user_data: string | null;
}
type ComputeModel = {
metadata: UserData;
interfaces: Interface[];
scripts: any;
};
export const ComputeModelSchema = implement<ComputeModel>().with({
// missing nullable, but doesn't error out
metadata: z.object({ user_data: z.string() }),
interfaces: z
.object({
// missing nullable, but doesn't error out
label: z.string(),
// missing "vlan", but it doesn't error out
purpose: z.enum(["public"]),
// extra object, but doesn't error out
extra: z.string().nullable(),
})
.array(),
// generates error:
// (property) test: z.ZodNullableType<z.ZodOptionalType<z.ZodType<any, z.ZodTypeDef, any>>>
// Property 'unwrap' is missing in type 'ZodAny' but required in type 'ZodNullable<ZodOptionalType<ZodType<any, ZodTypeDef, any>>>'.ts(2741)
scripts: z.any(),
});
type ComputePayload = z.infer<typeof ComputeModelSchema>;
@soupdash Try this :
// String litteral
type InterfacePurpose = "public" | "vlan";
// Real enum
enum EnumPurpose {
public,
vlan
}
interface Interface {
label: string | null;
purpose: EnumPurpose;
}
interface UserData {
user_data: string | null;
}
type ComputeModel = {
metadata: UserData;
interfaces: Interface[];
scripts: any;
};
export const ComputeModelSchema = implement<ComputeModel>().with({
// missing nullable, but doesn't error out
metadata: implement<UserData>().with({ user_data: z.string().nullable() }),
interfaces: z.array(implement<Interface>().with({
// missing nullable, error out
label: z.string().nullable(),
// missing "vlan", but it's expected because it's not an enum but a string literal
purpose: z.nativeEnum(EnumPurpose),
// extra object, error out
extra: z.string().nullable(),
})),
// generates error:
// (property) test: z.ZodNullableType<z.ZodOptionalType<z.ZodType<any, z.ZodTypeDef, any>>>
// Property 'unwrap' is missing in type 'ZodAny' but required in type 'ZodNullable<ZodOptionalType<ZodType<any, ZodTypeDef, any>>>'.ts(2741)
scripts: z.any().optional(),
});
type ComputePayload = z.infer<typeof ComputeModelSchema>;
For purpose
, when using a string literal you have to provide at min one value but can't require all. To do that, your enum have to be a "true" enum.
For "any", I have no idea but I try to avoid any
and unknown
in my schema π¬
@rphlmr Thanks. It's working better now, but unfortunately I'm pulling in third-party types that uses any/unknown. I'm assuming for my use case, it would be better to craft the zod schemas manually or use the ts-to-zod package?
As mentioned, the approach in https://github.com/colinhacks/zod/issues/372#issuecomment-1280054492 seems to fail for default values:
type T = {
n: number;
};
const zT = implement<T>().with({
n: z.number().default(5),
});
Type 'ZodDefault<ZodNumber>' is not assignable to type 'ZodType<number, ZodTypeDef, number>'.
Types of property '_input' are incompatible.
Type 'number | undefined' is not assignable to type 'number'.
Type 'undefined' is not assignable to type 'number'.ts(2322)
I think this can be fixed by a small modification to the Implements
type:
type Implements<Model> = {
[key in keyof Model]-?: undefined extends Model[key]
? null extends Model[key]
? z.ZodNullableType<z.ZodOptionalType<z.ZodType<Model[key]>>>
: z.ZodOptionalType<z.ZodType<Model[key]>>
: null extends Model[key]
? z.ZodNullableType<z.ZodType<Model[key]>>
: z.ZodType<Model[key]> | z.ZodDefault<z.ZodType<Model[key]>>;
};
Team -- I dug a little deeper with this and mashed it up with https://github.com/colinhacks/tozod.
See this PR for more: https://github.com/colinhacks/tozod/pull/28/files
It's still not super-robust, but it will disallow unknown keys and works with defaults:
type Example = {
n: number;
s: string;
b: boolean;
bi: bigint;
d: Date;
a: string[];
};
const zExample = implement<Example>().with(
z.object({
a: z.array(z.string()).default(['a', 'b', 'c']),
d: z.date().default(new Date()),
s: z.string().default('string'),
n: z.number().default(808),
b: z.boolean().default(true),
bi: z.bigint().default(BigInt(100)),
}),
);
type _ = Expect<Equal<z.infer<typeof zExample>, Example>>;
I'm trying to make a required field where the value can be undefined, which maybe zod can't do?. Despite the zod bug, I get the same unwrap
error when try to implement it:
const schema = implement<MySchema>().with({
description: z.string().or(z.undefined()),
});
Property 'unwrap' is missing in type 'ZodUnion<[ZodString, ZodUndefined]>' but required in type 'ZodOptional<ZodType<string | undefined, ZodTypeDef, string | undefined>>'.ts(2741)
types.d.ts(767, 5): 'unwrap' is declared here.
const customObject = <Base>(
shape: Record<keyof Base, ZodTypeAny>,
params?: z.RawCreateParams
) => z.object(shape as ZodRawShape, params);
As an alternative to the solution posted in https://github.com/colinhacks/zod/issues/372#issuecomment-1280054492 , with the main focus of working with zod
objects and typescritp
types/interfaces:
const parse = <T extends object>() => ({
with: <S extends { [P in keyof S]: P extends keyof T ? ZodTypeAny : never } & Record<keyof T, ZodTypeAny>>(
shape: S
) => z.object(shape)
})
// ? Example
interface Person {
name: string
age: number
}
const parser = parse<Person>().with({
name: z.string(),
age: z.number() // removing this line will prompt TS to complain
// address: z.string() // adding this line will prompt TS to complain
})
parser.parse({ name: 'Juanito', age: 35})
Feels simpler to me in terms of not needing to define custom complex / nested generic types, and following zod
's parse, don't validate nomenclature π
Is there anyway to make https://github.com/colinhacks/zod/issues/372#issuecomment-1280054492 work when nested properties are added by mistake?
For example:
interface Person {
age: number;
name: {
first: string;
last: string;
};
}
export const test = implement<Person>().with({
age: z.number(),
mistake: z.string(), // This prompts TS to complain
name: z.object({
first: z.string(),
last: z.string(),
nestedMistake: z.string(), // This does NOT prompt TS to complain
}),
});
Are we at some solid solution?
any solution here ?
TL;DR
I would love to have a way to ensure that a Zod schema matches an existing type definition using the normal Typescript type checker. Below is an example of the desired functionality using one possible implementation stolen from this comment.
Introduction
I originally raised this suggestion in #53, but decided it probably needs its own issue. See my original comments here and here. Based on the reactions to these comments, there are at least a few other people thinking along the same lines as I am. I will restate my thoughts below.
I want to start by saying that Zod is a really, really cool project. It is the best runtime validation system for typescript by far. I hope the following critiques are constructive.
My runtime validation requirements
I started implementing Zod in my project, and I went into this implementation assuming that Zod would meet the following two requirements:
In order to get the effect of my second requirement, I discovered that I need to replace my existing code, eg...
...with something like this...
This makes it so that if I change
aSchema
,A
will automatically update to match, which gives me most of what I was looking for. But there are some serious problems with this solution.The Problems
The most obvious problem with the above code example is that it removes some really valuable typescript features: As just one example, the functionality of
readonly
has been lost in the conversion toaSchema
. Perhaps it is possible to reintroduce that functionality with some fancy Typescript manipulation, but even if that is the case it is still not ideal.Perhaps a more central problem, though, is that I need to strip out pretty much all of my current type definitions and replace them with Zod schemas. There are some tools out there that will do this work for you (issue #53 was originally and ultimately about building these sorts of tools), but the real issue for me isn't the work of refactoring: The real problem is that such a refactor puts Zod in charge of my type system, which is very undesirable. In my opinion, Typescript should be the master of typing, and Zod should be the master of validation. In the current system, Typescript is subordinated to Zod rather than the other way around.
To make sure my intent is clear, here are a few re-statements of this idea:
To put it a different way, Zod is advertised as "Typescript first", but right now it feels more like "Zod first with Typescript as a close second". I say that because, currently, if I want to maintain type consistency I have to write the Zod schemas first, then use them to generate types. To be truly "Typescript first", the schemas should conform to the types instead of the types being generated from the schemas.
The
tozod
solutionA great idea that addresses these issues was introduced in this comment, discussed in this comment, then partially implemented in the
tozod
library (see this comment; the library can be found here). Thetozod
utility allows me to write the following in place of the above code example:This meets my requirements perfectly. It preserves my original types and has a schema that conforms to those types. It gives me the same strong typing as using
z.infer
would have, but it leaves Typescript in charge of defining my type system and still gives me all the benefits of runtime validation. It also preserves certain Typescript functionality that can't be modeled in Zod, like thereadonly
inA
. I think this scenario gives the best of all worlds and is truly "Typescript-first". I realize that it is slightly more verbose than just having a schema, but I think the benefits are well worth it. It fits much better into the established Typescript paradigm. I could go on and on about why this is a better solution.The problem with
tozod
There is just one problem with the
tozod
utility. I quickly discovered, and the author oftozod
admits, that it is "pretty fragile" and can only handle the most basic types (see this comment). Even a simple union type will cause it to break. Thetozod
library was a great step in the right direction, but in its current state it is virtually unusable in real application environments.My suggestion
My suggestion is that we need something like
tozod
, preferably built intoZod
, that allows schemas to be type checked against existing types in a real application environment. I don't know if this is feasible -- I'm not a Typescript expert, so it might not even be possible; but if it is possible, I think this change would be extremely beneficial.