5

I have been using Zod validation library with typescript for a little while and been loving it.

This might be a very basic thing, but I am trying to figure out what would be best pattern to extend Zod Schema with class-like functionality.

For the sake of the example lets have a Vector3 schema like this:

const Vector3Schema = z.object({
    x: z.number(),
    y: z.number(),
    z: z.number(),
})

type Vector3 = z.infer<typeof Vector3Schema>;

This is very nice to have type associated with the zod validation. No duplicate definitions, extremely clean. I like it.

The problem is how I should go on about extending Vector3 type with some class-like functionality.

Maybe this is my OOP background shining trough, but I would like to have some basic functionality associated with the type in a class-like manner, so it could be used like this:

let vec1 = Vector3Schema.parse({x:1, y: 2, z: 3});
let vec2 = Vector3Schema.parse({x:4, y: 5, z: 6});
let distance = vec1.distance(vec2);
vec1.normalize();

I could just create a separate class definition for Vector3 and use Zod only for validating the data passed to the constructor. But this will lead quickly to the same old problem: having to maintain class definition and validation logic separately.

So I guess the question is: is there a convenient way to parse Zod schema to a class instance? Or alternatively extend parsed type with additional functionality?

Or.. should I just let go of my grampa OOP ways and move forward with more functional style:

function normalize(v: Vector3) {...}
function distance(v1: Vector3, v2: Vector3) {...}

let vec1 = Vector3Schema.parse({x:1, y: 2, z: 3});
let vec2 = Vector3Schema.parse({x:4, y: 5, z: 6});
let distance = distance(vec1, vec2);
vec1 = normalize(vec1);

I would love to hear your ideas on this topic.

Gwydion
  • 252
  • 1
  • 7

1 Answers1

4

Looks like this has been discussed for Javascript in here: https://github.com/colinhacks/zod/issues/641

The suggested idea is to use factory function to extend the object. Maybe this could be modified to use with typescript. Need to look into this.

Update:

After testing a bit I come up with this solution for typescript:

  const Vector3Schema = z.object({
    x: z.number(),
    y: z.number(),
    z: z.number(),
  });

  interface Vector3 extends z.infer<typeof Vector3Schema> {
    normalize: () => void;
    distance: (v: Vector3) => number;
  }

  function parseVector3(data: unknown): Vector3 {
    return {
      ...Vector3Schema.parse(data),
      normalize: function () { /*...*/ },
      distance: function(v: Vector3) { /*...*/}
    };
  }

Now the usecase working with all typescript goodness would be:

let vec1 = parseVector3({x:1, y: 2, z: 3});
let vec2 = parseVector3({x:4, y: 5, z: 6});
let distance = vec1.distance(vec2);
vec1.normalize();

Please let me know, if there is better or more elegant solution to the problem.

Gwydion
  • 252
  • 1
  • 7