Subclassing in Zod
Zod is mostly written with classes. There's a base Z.ZodType
class that pretty much every other class inherits from.
Colin, the creator of Zod, demonstrates how this pattern's approach to generics provides complexity boundaries.
Resources
Transcript
0:00 Let's go on a little diversion and show you something really cool about classes that Colin, the creator of Zod, taught me. Zod is mostly written with classes.
0:09 They have a base class which is called z.ZodType, and pretty much every other class that comes from it, so ZodString, ZodNull, ZodNumber, inherits from that base class. This gives you a really interesting approach when it comes to generics.
0:23 Something that's interesting here about the subclass approach is that it gives you these complexity boundaries, which is really nice. When you create a ZodString schema and you hover over it in VS code, it just says ZodString, which is really valuable, I think. Z.ZodString, that's really nice.
0:43 This subclassing is one of the few patterns in TypeScript that lets you really represent complex things, but then it's hidden in the IntelliSense, basically.
0:54 If I just did something, if I represented this instead with subclasses as just with the type keyword, and I just had, let's say I have a type, which is just all of the methods defined on the base class, and then I have additional methods or something that are specific to the string schema, and the way that I represent the result of z.String is I union those together, that is similar to subclassing in some sense.
1:22 You've got the base methods frm one type, you've got all the other stuff defined in another type, and you just mush them together. That basically, would be equivalent, but the way that TypeScript would represent that to you is as this horrifying union of two very complicated object types. It would just be completely unintelligible.
1:41 Here's an example of what Colin is talking about. You would have something like ZodString here, which would be a type object with a bunch of different methods on it. Imagine 20, 30 methods, however many ZodString has, then you would intersect it with ZodType where you actually have the base type here, which has all the base methods.
2:00 Here we're passing in String just like ZodString does to ZodType, and then you would have this here too. When you actually use this, so imagine we have this String here, you do actually when you hover over the thing that you've created at this, you get ZodString, which is quite nice to read.
2:17 If you hover over this, then imagine this, it spills out into everything, and so you get the raw definition here and this funny intersection here. This does not scale in terms of complexity very well. Colin's found this really cool approach with this subclassing, that lets you be really, really specific with how you want it to appear in the IntelliSense.
2:38 There's this two-level generic pattern. ZodString is simplified here because ZodString itself is not generic, which is interesting. It's just, ZodString, no generic parameters after that, but it extends that superclass, and then you just pass in all of those generics that correspond to in this scenario of what the def type is and then what the actual inferred type is, which of course is just String.
3:03 There you go. This gives you a method for hiding generic complexity in your libraries if that's what you really want. It means that actually using classes can be used as a way to make small things internal in your type system if you need them to.