question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Simple `ZodObject` inference from existing Interface?

See original GitHub issue

When needing to attempt to bind an existing Interface to ZodObject, there are a few issues with using the more simplistic ZodSchema to achieve this - namely losing all the object-specific options such as schema.shape.

As far as I know, there isn’t really a way to neatly infer a ZodObject without needing to recapitulate the entire schema again within the definition, which is often not possible when the source type is external and the point of the binding is to notice if the shape of the schema differs from the source.

This is what I (very roughly) have worked out so far, so was wondering if a) there is any existing internals that might be better suited to this, and; b) if there is any chance some more developed version of this inference type might be possible to include in the main package?

/**
 * Simple example mapping to string version of each type, would need to be completed with all types
 */
export type GetRequiredType<T> = T extends string
  ? "string"
  : T extends number
  ? "number"
  : T extends boolean
  ? "boolean"
  : never

/**
 * Append `Optional` to the key name for optional properties.
 */
export type GetType<T> = undefined extends T
  ? `${GetRequiredType<T>}Optional`
  : GetRequiredType<T>

/**
 * Simple example mapping / lookup of "rough" Zod types for each concrete type.
 */
export interface ZodTypes {
  string: ZodString | ZodEffects<ZodString, string, any>
  stringOptional: ZodOptional<ZodString | ZodEffects<ZodString, string, any>>
  number: ZodNumber | ZodEffects<ZodNumber, number, any>
  numberOptional: ZodOptional<ZodNumber | ZodEffects<ZodNumber, number, any>>
  boolean: ZodBoolean | ZodEffects<ZodBoolean, boolean, any>
  booleanOptional: ZodOptional<
    ZodBoolean | ZodEffects<ZodBoolean, boolean, any>
  >
}

/**
 * Cast the existing output interface as a ZodRawShape using the lookups defined above.
 */
export type ToZodRawObject<Output extends object> = {
  [Key in keyof Output]: ZodTypes[GetType<Output[Key]>]
}

/**
 * Case the existing output interface as a valid ZodObject.
 */
export type ToZodObject<Output extends object> = ZodObject<
  ToZodRawObject<Output>,
  "strip",
  ZodTypeAny,
  Output
>

Passing Example

export interface User {
  name?: string
  email: string
}

export const UserSchema: ToZodObject<User> = z.object({
  name: z.string().optional(), // Passes
  email: z.string().email() // Passes
})

Failing Example

export interface Product {
  name: string
  serial?: number
}

export const ProductSchema: ToZodObject<Product> = z.object({
  name: z.string().transform((value) => value.toUpperCase()), // Passes
  serial: z.number().refine((value) => value > 1000) // Fails! (must be optional)
})

Issue Analytics

  • State:closed
  • Created a year ago
  • Reactions:2
  • Comments:8

github_iconTop GitHub Comments

5reactions
cyrilchaponcommented, Dec 14, 2022

IMHO this should be reopened, as

  • The use-case is very basic and common
  • The trouble is not resolved as of today
  • and toZod package is outdated

I can expand on the actual use-case, in a simple form :


Traditional zod usage

It often start with the schema defined. Later, the underlying, actual, data type of the object can be derived with z.infer

import { z } from 'zod'

export const BearZ = z.object({
  id: z.string(),
  nickName: z.string(),
})

type Bear = z.infer<typeof BearZ>

Problematic usage

There are circumstances where the actual type is already defined and derived from somewhere else. A good example is a database model.

The actual trouble is, defining a random z.object in that case does not have the benefit to typecheck the schema versus the “wanted” type :

import { z } from 'zod'
import { Bear } from './db/models/bear'

export const BearZ = z.object({
  id: z.string(),
  nickName: z.string(),
  // ⚠️ No autocompletion here. No hint on how to type the schema
})

// BearZ output right here can lead be completely different type than Bear

A common workaround is to force the type, with z.ZodType :

import { z } from 'zod'
import { Bear } from './db/models/bear'

export const BearZ: z.ZodType<Bear> = z.object({
  id: z.string(),
  nickName: z.string(),
})

This works for autocompletion, but leads to several other issues. A good example is a real-world situation where you will re-use the schema in other schemas; such as creation or update payloads.

Consider the following

import { z } from 'zod'
import { Bear } from './db/models/bear'

export const BearZ: z.ZodType<Bear> = z.object({
  id: z.string(),
  nickName: z.string(),
})
export const BearCreationZ = BearZ.omit({ id: true })

Pretty cool uh ? Here you can convince yourself it will throw on build, because BearZ here is actually not a ZodObject ! (and .pick relies on ZodObject).


So let’s try using ZodObject directly maybe ? With something like :

export const BearZ: z.ZodObject<Bear> = z.object({
  id: z.string(),
  nickName: z.string(),
})

This doesn’t work written like this, because the index signature of z.object is not that simple :

declare const objectType: <T extends ZodRawShape>(shape: T, params?: RawCreateParams) => ZodObject<T, "strip", ZodTypeAny, { [k_1 in keyof objectUtil.addQuestionMarks<{ [k in keyof T]: T[k]["_output"]; }>]: objectUtil.addQuestionMarks<{ [k in keyof T]: T[k]["_output"]; }>[k_1]; }, { [k_3 in keyof objectUtil.addQuestionMarks<{ [k_2 in keyof T]: T[k_2]["_input"]; }>]: objectUtil.addQuestionMarks<{ [k_2 in keyof T]: T[k_2]["_input"]; }>[k_3]; }>;

In that particular situation; I come to the conclusion that :

  • Having typechecked schema against exiting type
  • Plus keeping the ZodObject type

is impossible.


Reference : #53, #372

4reactions
nikfpcommented, Jun 19, 2022

You might take a look at toZod, which is a simple utility that allows you to input a type or interface as a generic argument, and then type checks your Zod schema as you develop against the input type and throws a ts error if you don’t align. This makes your external type drive the Zod schema instead of the other way around, and you dont have to remember to keep them in sync. I’m using it with types generated from graphql-code-generator for mutation inputs and so far haven’t had any issues.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Designing the perfect Typescript schema validation library
Introducing Zod · Uses Typescript generic inference to statically infer the types of your schemas · Eliminates the need to keep static types...
Read more >
Doubts about type inference when using pick of zodObject
when calling the pick method of a zodObject in a function by passing in a reference, how to set the type of T...
Read more >
TypeScript-First Schema Validation with Static Type Inference
With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type. It's easy to compose simpler types into...
Read more >
Schema validation in TypeScript with Zod - LogRocket Blog
Let's start talking about schema validation with a very basic Zod ... want a newly created variable to deduce its type from existing...
Read more >
Untitled
nativeEnum()`, which lets you create z Zod schema from an existing ... refer to any data type/structure, from a simple `string` to a...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found