Search code examples
typescriptzod

Enforce Base Schema Type in Generic Constraint


Is it possible for Zod to enforce that a passed schema's type is constrained to a base schema? I suspect I might be way off base here, but hopefully it is clear what I'm trying to achieve in the example.

Thank you.

import { ZodType, z } from "zod";

const baseSchema = z.object({baseString:z.string()})
const validChildSchema = baseSchema.extend({childString:z.string()})
const invalidChildSchema = z.object({childString:z.string()})

class baseClass<TSchema> // eg something like  "extends ZodType<typeof baseSchema>"
{}

class childClassValid extends baseClass<typeof validChildSchema>{} 

// Can I change the baseclass so that this does not compile?
class childClassInvalid extends baseClass<typeof invalidChildSchema>{} 

Solution

  • I think you can achieve something like this by constraining the "output" type of the generic parameter to be a the inferred output type of the base schema.

    Eg:

    import { z } from "zod";
    
    const baseSchema = z.object({ baseString: z.string() });
    const validChildSchema = baseSchema.extend({ childString: z.string() });
    const invalidChildSchema = z.object({ childString: z.string() });
    
    class BaseClass<TSchema extends z.ZodType<z.infer<typeof baseSchema>>> {
      constructor(public schema: TSchema) {}
    }
    
    class ChildClassValid extends BaseClass<typeof validChildSchema> {}
    
    const v = new ChildClassValid(validChildSchema);
    
    const out = v.schema.parse("");
    console.log(out.baseString);
    console.log(out.childString);
    
    // Property 'baseString' is missing in type '{ childString: string; }' but required in type '{ baseString: string; }'.
    class ChildClassInvalid extends BaseClass<typeof invalidChildSchema> {}
    

    Typescript Playground link

    Due to Typescripts structural typing, this won't prevent you from using schemas like:

    const anotherSchema = z.object({baseString: z.string(), foo: z.number()})
    

    So if that's a concern then you'll need to play around with branded types to simulate nominal typing.