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.

Typecheck schemas against existing types

See original GitHub issue

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.

type Dog = {
  name: string
  neutered: boolean
}

//Passing "Dog" as a generic type parameter tells Typescript what the schema should look like
const dogSchema = z.object<Dog>({
  name: z.string().min(3),
  neutered: z.string(), //Error: string can't be assigned to boolean
});

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:

  1. Zod schemas would provide run time checks of my data types. [true]
  2. Zod schemas would conform to my existing types, so that it is impossible to change the type without also changing the associated schema (and vice versa) . [only sort of true]

In order to get the effect of my second requirement, I discovered that I need to replace my existing code, eg…

export interface A {
   readonly ID: number;
   delayEnd: number;
   userID: number;
   reason: string;
   taskID: number;
   initiationDate: number;
   days?: number;
   userName?: string;
}

…with something like this…

export const aSchema = z.object({
   ID: z.number(), //Note that I've lost the functionality of `readonly` in this conversion
   delayEnd: z.number(),
   userID: z.number(),
   reason: z.string(),
   taskID: z.number(),
   initiationDate: z.number(),
   days: z.number().optional(),
   userName: z.string().optional()
});

//"A" is generated from the schema
export type A = z.infer<typeof aSchema>;

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 to aSchema. 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:

  • I want to keep all my types as they are and create schemas that conform to them.
  • I do not want to replace my existing type definitions with schemas; instead I want to create schemas that match my existing types.
  • I want to keep my type system in typescript and only use Zod to validate that objects fit my existing types.

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 solution

A 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). The tozod utility allows me to write the following in place of the above code example:

//My interface does not change
export interface A {
   readonly ID: number;
   delayEnd: number;
   userID: number;
   reason: string;
   taskID: number;
   initiationDate: number;
   days?: number;
   userName?: string;
}

//The use of toZod ensures that the schema matches the interface
export const aSchema: toZod<A> = z.object({
   ID: z.number(),
   delayEnd: z.number(),
   userID: z.number(),
   reason: z.string(),
   taskID: z.number(),
   initiationDate: z.number(),
   days: z.number().optional(),
   userName: z.string().optional()
});

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 the readonly in A. 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 of tozod 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. The tozod 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 into Zod, 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.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:32
  • Comments:29 (5 by maintainers)

github_iconTop GitHub Comments

28reactions
colinhackscommented, Apr 25, 2021

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

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(),
  })
);

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 anys 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

24reactions
rphlmrcommented, Oct 16, 2022

Thanks to @ridem I made a version that error if a schema property doesn’t exist in the type to implement.

  • handles requirement for .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

Read more comments on GitHub >

github_iconTop Results From Across the Web

Type Checking - Open Policy Agent
Using schemas to enhance the Rego type checker. You can provide one or more input schema files and/or data schema files to opa...
Read more >
Schema validation in TypeScript with Zod - LogRocket Blog
In this article, you will learn about schema design and validation in Zod and how to run it in a TypeScript codebase at...
Read more >
Zod: create a schema using an existing type - Stack Overflow
The other shortcoming to this approach is that something like this will typecheck: const methods z.ZodType<Method> = z.enum(['get']);.
Read more >
Generating types from a GraphQL schema
We'll use the GraphQL Code Generator library to generate types based on our GraphQL schema. There are multiple ways to provide a schema...
Read more >
typescript dynamic type check using interface, json schema ...
More about me https://nathankrasney.com/קורסים בטייפסקריפט בעברית ...
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