helion3
helion3

Reputation: 37431

Declaring custom functions on a third party abstract class

I need to extend Zod with some custom methods. They don't really have a plugin system so other plugins I've seen use the prototype. This works as intended:

z.ZodType.prototype.disabled = function(isDisabled: boolean): ZodType<any, ZodTypeDef, any> {
  this._isDisabled = isDisabled
  return this
}

z.ZodType.prototype.isDisabled = function(): boolean {
  return this._isDisabled
}

The problem is that we use Typescript, and the original ZodType abstract class obviously doesn't define these extension methods.

I was thinking I could get away with typescript's "interface merging" to extend this, like some other stackoverflow posts recommend, but this isn't an interface - it's an abstract class type.

interface ZodType {
  disabled: (isDisabled: boolean) => void
  isDisabled: () => boolean
  _isDisabled: boolean
}

This doesn't do anything. Here's an example using my custom function:

const schema = z.object({
  name: z.string().min(3),
  disabled: z.string().disabled(true)
})

This errors with "property 'disabled' does not exist on type ZodString" yet ZodString inherits from ZodType

Is there a way I can properly tell typescript about these changes?

Upvotes: 3

Views: 452

Answers (1)

jcalz
jcalz

Reputation: 329308

You can definitely merge into the instance side of class declarations. A class declaration like class Foo { /*...*/ } brings into scope both the class constructor value named Foo, and the class instance interface named Foo, and you can merge into that interface. This is true for abstract classes as well. So if the class name is ZodType then you need to merge into interface ZodType {}.

Since that interface is in a module, you're looking for module augmentation. That's where you import a module and then merge your definition into an interface inside an appropriate declare module scope. Like this:

import * as z from 'zod';

declare module 'zod' {
    interface ZodType {
        disabled(isDisabled: boolean): this;
        isDisabled(): boolean;
        _isDisabled: boolean
    }
}

And now the compiler will know about these definitions, allowing the rest of your code to type check with no errors:

z.ZodType.prototype.disabled = function (isDisabled: boolean) {
    this._isDisabled = isDisabled
    return this
}

z.ZodType.prototype.isDisabled = function (): boolean {
    return this._isDisabled
}

const schema = z.object({
    name: z.string().min(3),
    disabled: z.string().disabled(true)
})

Playground link to code

Upvotes: 1

Related Questions