zod
zod copied to clipboard
How to use Zod schema with existing TypeScript interface?
Discussed in https://github.com/colinhacks/zod/discussions/2796
Originally posted by adamerose September 24, 2023 I see Zod described as a "TypeScript-first" library but am struggling to figure out how to actually integrate it with my TypeScript definitions. Is there a way to pass a TypeScript interface to my Zod schema as a generic and have my linter tell me if they're not compatible?
This is how it looks in Yup:
import { object, number, string, ObjectSchema } from 'yup';
interface Person {
name: string;
age?: number;
sex: 'male' | 'female' | 'other' | null;
}
// will raise a compile-time type error if the schema does not produce a valid Person
const schema: ObjectSchema<Person> = object({
name: string().defined(),
age: number().optional(),
sex: string<'male' | 'female' | 'other'>().nullable().defined(),
});
// ❌ errors:
// "Type 'number | undefined' is not assignable to type 'string'."
const badSchema: ObjectSchema<Person> = object({
name: number(),
});
I found discussion here and here but didn't see any solution I could understand, or relied on codegen and external libraries.
I think something like this might work? But it's very boilerplatey and I don't see anything like this in the docs:
interface Person {
name: string;
age?: number;
sex: "male" | "female" | "other" | null;
}
const zodSchema = z.object({
name: z.string(),
age: z.number().optional(),
sex: z.enum(["male", "female", "other"]).nullable(),
});
type InferredPerson = z.infer<typeof zodSchema>;
function assertType<T>(_value: T) {}
assertType<Person>({} as InferredPerson);
```</div>
In practice (assuming that you actually do something with your parsed data), wouldn't code like this be enough to do such a check?
import z from 'zod';
interface Person {
name: string;
age?: number;
sex: "male" | "female" | "other" | null;
}
function doSomethingWithPerson(person: Person) {
return person;
}
const badSchema = z.object({
name: z.number(),
});
const personBad = badSchema.parse({ name: 'foo' });
doSomethingWithPerson(personBad);
// ❌ Argument of type '{ name: number; }' is not assignable to parameter of type 'Person'.
// Property 'sex' is missing in type '{ name: number; }' but required in type 'Person'.(2345)
This will throw an error if your schema does not match the interface.
I'm not sure why you would need an additional assertion, but if you do, code like you suggested would be fine for that.
I'm also wondering how this can be achieved. I have an existing interface I'd like to make sure that my zod schema always matches. It's easy enough to run my existing interface thru a zod schema generator and generate the matching zod schema but that does not ensure it stays in sync over time. If the original interface is updated, I want a compile time error telling me to update my zod schema. This is is possible with the io-ts library but I am struggling to figure out how to do it with zod.
Adding my two cents to this. I also don't see any solution actually being given in issues such as #53.
I don't want Zod to determine what my TypeScript types and interfaces look like, I want my TypeScript types and interfaces to determine what Zod looks like.
I would like:
interface Car {
make: string;
model: string;
}
To lead to:
z.object({ make: z.string(), model: z.string() });
Not the other way around.
As awesome as Zod is, and how eager I was to use it, I put more trust into TypeScript being around and maintained for a long time than I do in Zod. If for any reason I need to move away from Zod in the future, if all my types are inferred through Zod, that becomes a real issue.
As eager as I was to use Zod on my current project, I will probably choose something else instead because of this design choice.
I think because TS is a superset of JS and an interface doesn't actually exist in JS-land, there's nothing tangible from an interface come transpile/run time. Ultimately there would need to be something that generated code at build, or even a code generator before build/run time that would take TS interfaces and build out the associated code validation (ie: Zod). Maybe there's some type of reflection that could be utilized to help do this?
Some interesting links around such:
- https://radzserg.medium.com/reflection-in-typescript-af68a1536ea1
- https://www.typescriptlang.org/docs/handbook/decorators.html#metadata
- https://stackoverflow.com/questions/41564078/typescript-reflection-for-interfaces
- https://typescript-rtti.org/
In practice (assuming that you actually do something with your parsed data), wouldn't code like this be enough to do such a check?
import z from 'zod'; interface Person { name: string; age?: number; sex: "male" | "female" | "other" | null; } function doSomethingWithPerson(person: Person) { return person; } const badSchema = z.object({ name: z.number(), }); const personBad = badSchema.parse({ name: 'foo' }); doSomethingWithPerson(personBad); // ❌ Argument of type '{ name: number; }' is not assignable to parameter of type 'Person'. // Property 'sex' is missing in type '{ name: number; }' but required in type 'Person'.(2345)
This will throw an error if your schema does not match the interface.
I'm not sure why you would need an additional assertion, but if you do, code like you suggested would be fine for that.
I use this solution and it has met my needs. The zod object is not based on my interface but I have the linter notifying me if it is in agreement
Hello, everyone.
Actually we can achieve the inverse. Instead of using TypeScript interface for Zod's schema, we can use the Zod's "infer" functionality to get TypeScript type definition from Zod's schema. Here is the example:
const PersonSchema = z.object({
name: z.string(),
});
export type Person = z.infer<typeof PersonSchema>;
The TypeScript type checking also works this way:
I think the prefered way of using it / the perfect solution would be something like:
//simple interface or type with any properties, most likely generated
interface Human {
name: string;
age?: number;
}
// to any zod-schema:
const schema = z.from<Human>();
// or even something like this would be OK:
class Person implements Human {
// ... constructor etc
}
const another = z.create(Person);
// you would parse as normal
const unknownData = {propA: "a", name: "john", age: "123"}
const parsed = schema.parse(unknownData);
// ^? = typeof Human
// same for .safeParse and safeParseAsync
Would that even be possible to extract properties and types from an interface?
Would that even be possible to extract properties and types from an interface?
This is kind of backwards and not at all the goal of zod. It is not possible to create runtime-code from TS without additional build steps. Zod is a JS library, not a build tool. But someone else could create such a "ts-to-zod" bundler plugin, which could use typescript AST parsing to generate new JS code at build time. However, then your TS types become the source of truth, and those are less expressive than what zod provides.
/edit: just saw that someone built this: https://github.com/fabien0102/ts-to-zod
However, then your TS types become the source of truth, and those are less expressive than what zod provides.
Let's say you have a database and an ORM to talk to it, that ORM has types but no validation. So what do you want: A zod validation over an existing type/class/object.
I think this could be done during runtime, how I am not sure.
Adding my two cents to this. I also don't see any solution actually being given in issues such as #53.
I don't want Zod to determine what my TypeScript types and interfaces look like, I want my TypeScript types and interfaces to determine what Zod looks like.
I would like:
interface Car { make: string; model: string; }
To lead to:
z.object({ make: z.string(), model: z.string() });
Not the other way around.
As awesome as Zod is, and how eager I was to use it, I put more trust into TypeScript being around and maintained for a long time than I do in Zod. If for any reason I need to move away from Zod in the future, if all my types are inferred through Zod, that becomes a real issue.
As eager as I was to use Zod on my current project, I will probably choose something else instead because of this design choice.
I'd like to add to this that it's not just about trust but also about source of truth being generated by other tools. Today many types get generated from for instance Prisma
or GQL
schemas. With zod
I have no clean mean of ensuring that schema structure is up to date with types that it's supposed to validate.
Not only that I can't have the schema implement certain interface but also argument of parse
is of type unknown
so there is no type safety at all.
This is still boilerplatey, but what about something like this?
interface Car {
make: string;
model: string;
}
const Car = z.object({
make: z.string(),
model: z.string()
}) satisfies ZodType<Car>
This guarantees that the zod schema you define is in sync with the interface that already exists. I've used this approach a few times when the type I'm modeling comes from some other space I don't control (codegen, upstream libraries, etc.).
This is still boilerplatey, but what about something like this?
interface Car { make: string; model: string; } const Car = z.object({ make: z.string(), model: z.string() }) satisfies ZodType<Car>
This guarantees that the zod schema you define is in sync with the interface that already exists. I've used this approach a few times when the type I'm modeling comes from some other space I don't control (codegen, upstream libraries, etc.).
It does, but at the same time you have to redefine your properties that are already set in the interface, but for zod.
Yeah, as @graup mentioned, I don't think that's a thing zod is going to handle itself; to generate zod parsers from static types you'll need codegen tools like ts-to-zod. My experience is that codegen tools tend to be painful to work with and lead to bad dev experiences, so I tend to opt for the statically checked repetition instead.
That may be because in my cases it's usually not properties that I've already set; if I'm doing this, it's because I'm trying to create a zod parser for a type I don't control. If I did control it, I'd just derive the type from the zod parser.
I've used the Joi
schema validator before, which has this exact feature - treating your TS types as the source of truth:
import * as Joi from 'joi'
interface Car {
make: string;
model: string;
}
const Car = Joi.object<Car>({
make: Joi.string(),
model: Joi.string()
})
Truth be told, it doesn't support the inverse (what Zod
does).
So, if you need your schema validation library to support defining a schema based on your TS type, you can look for alternatives like Joi
.
However, it would be awesome if Zod
supported this workflow as well.
I've used the
Joi
schema validator before, which has this exact feature - treating your TS types as the source of truth
@Papooch It ensures only that you don't declare additional keys. It doesn't ensure everything else. In your example, Joi is happy with a wrong schema:
const Car = Joi.object<Car>({
make: Joi.boolean(), // wrong type
// missing key `model`
});
@paleo My experience is different with version 17.13.1
: If I enable the strict
type check (which I'm not sure it's false
by default), I get a pretty decent type-check.
For example, the following behaves as you describe:
interface Car {
make: string
model: string
}
const Car = Joi.object<Car>({
make: Joi.boolean(),
model: Joi.string(),
})
However if I switch to:
const Car = Joi.object<Car, true>({ // <--- notice the "true" here
make: Joi.boolean(),
model: Joi.string(),
})
gives me a
Type 'BooleanSchema<boolean>' is missing the following properties from type 'StringSchema<string>': alphanum, base64, case, creditCard, and 24 more.ts(2740)
I found this: https://github.com/jbranchaud/til/blob/master/zod/incorporate-existing-type-into-zod-schema.md The author uses zodType, such as:
const customSchema: z.zodType<CustomType> = z.any();
const schema = z.object({
custom: customSchema.array();
})
@Athrun-Judah this will only validate that the key custom
maps to an array; it doesn't validate that the elements of the array match CustomType
, it just assumes that they do. That might be fine, but is no safer than casting the elements of the array like this;
type CustomType = {a: 'particular shape'}
const schema = z.object({custom: z.any().array()})
const unvalidated = {custom: ['anything', 3, {at: 'all'}, null,undefined]}
const validated: {custom: CustomType[]} = schema.parse(unvalidated)
I wrote a tool https://github.com/ylc395/zodify
You can use this tool to generate schemas from TypeScript types.
satisfies z.ZodType<User>
seems to be working well, but it doesn't tell you the missing key exactly.
I'm using a mapped type by defining ToZodSchema
. It works well with nullable and optional.
type IsNullable<T> = Extract<T, null> extends never ? false : true
type IsOptional<T> = Extract<T, undefined> extends never ? false : true
{
type T1 = IsNullable<string> // false
type T2 = IsNullable<string | null> // true
type T3 = IsNullable<string | undefined> // false
type T4 = IsNullable<string | null | undefined> // true
type T5 = IsOptional<string> // false
type T6 = IsOptional<string | null> // false
type T7 = IsOptional<string | undefined> // true
type T8 = IsOptional<string | null | undefined> // true
}
type ZodWithEffects<T extends z.ZodTypeAny> = T | z.ZodEffects<T, unknown, unknown>
export type ToZodSchema<T extends Record<string, any>> = {
[K in keyof T]-?: IsNullable<T[K]> extends true
? ZodWithEffects<z.ZodNullable<z.ZodType<T[K]>>>
: IsOptional<T[K]> extends true
? ZodWithEffects<z.ZodOptional<z.ZodType<T[K]>>>
: ZodWithEffects<z.ZodType<T[K]>>
}
interface Foo {
bar: string
withEffects: string
nullable: number | null
optional?: string
optionalNullable?: string | null
omitted?: string
}
const schema = z.object({
bar: z.string(),
withEffects: z.preprocess(val => val, z.string()),
// bar: z.number() // Type 'ZodNumber' is not assignable to type 'ZodType<string, ZodTypeDef, string>'.
// baz: z.number(), // Object literal may only specify known properties, and 'baz' does not exist in type 'Schema<Foo>'.ts(2353)
nullable: z.number().nullable(),
optional: z.string().optional(),
optionalNullable: z.string().optional().nullable(), // should chain optional first
// omitted: z.string().optional(),
} satisfies ToZodSchema<Foo>) // error: Property 'omitted' is missing in type
any update on this ? how can i use existing interface to construct my zod schema
@LeulAria There have been multiple suggested solutions in this thread. The TL;DR is that this isn't something that zod does but it can be done with other tools.
I think this might work - z.custom
interface IRate {
id: string;
name: string;
}
const LocationFormSchema = z.object({
name: z.string().min(1, { message: 'Name is required and cannot be empty' }),
rate: z.custom<IRate>().optional(),
});
in this my rate
object is defined based on the interface IRate
and when assigning a rate in a zod object does not gives any type error