Yup: Does anyone have an example of `addMethod` in Typescript?

Created on 16 Sep 2018  路  11Comments  路  Source: jquense/yup

Context: I'm using Yup with Typescript and formik. Trying to add a custom method to enforce the format of a date value. The Yup README has an example, but I'm not sure how to get it to work with TS. Does anyone have an example? Thanks.

Most helpful comment

So I stumbled upon this thread and since no one has given a working answer for all questions asked there, here is what I did to make it work with typescript :

Trying to add this:

import { addMethod, date, DateSchema } from 'yup'

function withinSelectableDays(this: DateSchema, selectableDays: number[]) {
  return this.transform((value: Date) => {
    return [...]
  })
}
addMethod(date, 'withinSelectableDays', withinSelectableDays)

Note this: DateSchema in order to remove a TS error if you have the flag noImplicitThis set to true. With an arrow function, using date().transform instead of "this" seems to work, however I have then a weird bug where the other date functions (min & max) are broken..

@egmanoj For a custom error you can return it directly as a string instead of the "invalidDate" var. All it needs is not to be a parse-able date so Yup sees it as an error.

Once done, we have a TS error when trying to use "withinSelectableDays". To makes things work, instead of using an ignore, you can register it manually: create a file "index.d.ts" in a subdirectory of your project (e.g. types/yup/index.d.ts) where you put this:

import { DateSchema } from 'yup'

declare module 'yup' {
  interface DateSchema {
    withinSelectableDays(selectableDays: number[]): DateSchema
  }
}

For your ts compiler to use this file, add the following in your tsconfig.json:

"compilerOptions": {
...
    "typeRoots": [ "./types", "./node_modules/@types"]
}

The compiler will merge the previous file (yup/index.d.ts) with yup types, you have now your new function along the native ones! Also, you have to add it in the exclude array (same file):

"exclude": [
...
    "types"
}

so as not to add this directory during compilation.

And it's finally working!

All 11 comments

It should be any different in typescript,since it's the same library, tho the type def might be wrong, I don't use typescript so it hard for me to know!

Here's how I understand it:

yup.addMethod modifies the prototype of the DateSchema type. This makes the newly added method (say, "format") available at runtime. TS enforces types at compile time. At that time "format" is not a method of DateSchema, leading to a compilation error.

I suspect there's a (obvious) way around this, but I'm not able to find it.

Here's what I have so far. Context: I'm trying to add a custom format method to yup.date.

Step 1: Augment the yup module

  • File mysrc/typings/yup/index.d.ts
import { DateSchema, DateSchemaConstructor } from "yup";

declare module "yup" {
    interface DateSchema {
        format(format: string): DateSchema;
    }
}

export const date: DateSchemaConstructor;

Step 2: Add the method

  • File mysrc/somewhere/schema.ts
import moment from "moment";
import { date, DateSchema } from "yup";

const invalidDate = new Date("");
yup.addMethod<DateSchema>(yup.date, "format", function (format: string) {
    return this.transform((value: DateSchema, input: string) => {
        const parsed = moment(input, format, true);
        return parsed.isValid() ? parsed.toDate() : invalidDate;
    });
});

export const schema = object().shape({
    myDate: date()
        .format("YYYY-MM-DD")
        .required("..."),
});

Unsolved Problem

How can I replace the function argument passed to yup.addMethod with a fat arrow function, without screwing up the this used inside it? I.e. how to get to something like this:

yup.addMethod<DateSchema>(yup.date, "format", (format: string) => {
    return ????.transform((value: DateSchema, input: string) => {
        const parsed = moment(input, format, true);
        return parsed.isValid() ? parsed.toDate() : invalidDate;
    });
});

@jquense How can I return a custom error message in this context? Thanks!

May be like this

import moment from "moment";
import { addMethod, date, DateSchema } from 'yup';

const invalidDate = new Date("");
addMethod<DateSchema>(date, "format", (format: string) => {
  return date().transform((value: DateSchema, input: string) => {
      const parsed = moment(input, format, true);

      return parsed.isValid() ? parsed.toDate() : invalidDate;
  });
});

@egmanoj
I managed to create my function:

export function moneyNotEmpty(this: yup.StringSchema, msg: string) {
  return this.test({
    name: 'moneyNotEmpty',
    message: msg,
    test: (value) => value ? value.replace('R$', '').trim() !== '0,00' : false,
  });
}

Then I can do:

Yup.addMethod(Yup.string, 'moneyNotEmpty', moneyNotEmpty);

But when I try to use it:
valueToAdd: Yup.string().required('脡 necess谩rio um valor para adicionar ao objetivo').moneyNotEmpty('aaaa'),

I get a TS error. Did you manage to use achieve something? any tips? I am //@ts-ignore for now

Y'all addMethod is some light sugar over modifying a prototype, on a constuctor function. I'm not very familiar with TS but I'd imagine you'd need to also extend the yup schema type def manually when using addMethod.

Alternatively, schema's can be extended via inheritance so it may be clearer for TS to do

class MyString extends yup.string {
   newMethod() ...
}

export () => new MyString()

So I stumbled upon this thread and since no one has given a working answer for all questions asked there, here is what I did to make it work with typescript :

Trying to add this:

import { addMethod, date, DateSchema } from 'yup'

function withinSelectableDays(this: DateSchema, selectableDays: number[]) {
  return this.transform((value: Date) => {
    return [...]
  })
}
addMethod(date, 'withinSelectableDays', withinSelectableDays)

Note this: DateSchema in order to remove a TS error if you have the flag noImplicitThis set to true. With an arrow function, using date().transform instead of "this" seems to work, however I have then a weird bug where the other date functions (min & max) are broken..

@egmanoj For a custom error you can return it directly as a string instead of the "invalidDate" var. All it needs is not to be a parse-able date so Yup sees it as an error.

Once done, we have a TS error when trying to use "withinSelectableDays". To makes things work, instead of using an ignore, you can register it manually: create a file "index.d.ts" in a subdirectory of your project (e.g. types/yup/index.d.ts) where you put this:

import { DateSchema } from 'yup'

declare module 'yup' {
  interface DateSchema {
    withinSelectableDays(selectableDays: number[]): DateSchema
  }
}

For your ts compiler to use this file, add the following in your tsconfig.json:

"compilerOptions": {
...
    "typeRoots": [ "./types", "./node_modules/@types"]
}

The compiler will merge the previous file (yup/index.d.ts) with yup types, you have now your new function along the native ones! Also, you have to add it in the exclude array (same file):

"exclude": [
...
    "types"
}

so as not to add this directory during compilation.

And it's finally working!

I've found that making declarations like that causes typings to be lost.

Take this schema for instance

export const personSchema = yup
  .object()
  .shape({ name: yup.string(), age: yup.number() })
  .allowedKeysOnly()

type Person = yup.InferType<typeof personSchema>
declare module 'yup' {
  // The type Person is {} which is incorrect
  interface ObjectSchema {
    allowedKeysOnly(this: ObjectSchema, ...ignoredKeys: string[]): ObjectSchema
  }

  // The type Person is { name: string, age: number } which is correct
  interface ObjectSchema<T> {
    allowedKeysOnly(this: ObjectSchema<T>, ...ignoredKeys: string[]): ObjectSchema<T>
  }
}

Searching for a solution I've come across a few different approaches so far.

Source: https://github.com/PolymathNetwork/polymath-ui/blob/17d1e8b9da215bfb5eb633f3397bcff84903abed/src/validator/index.ts

Basically the author creates constructor functions like:

type ValidatorFn<T> = (
  message?:
    | string
    | ((params: object & Partial<Yup.TestMessageParams>) => string)
    | undefined
) => T;

interface CustomStringSchema extends Yup.StringSchema {
  isEthereumAddress: ValidatorFn<this>;
  isRequired: ValidatorFn<this>;
  required: ValidatorFn<this>;
  isEmail: ValidatorFn<this>;
  isUrl: ValidatorFn<this>;
}

interface CustomStringSchemaConstructor extends Yup.StringSchemaConstructor {
  (): CustomStringSchema;
  new (): CustomStringSchema;
}
// ...

And then extends the validator:

// ...
const yupValidator: ExtendedYupType = {
  ...Yup,
  bool: Yup.bool as CustomBooleanSchemaConstructor,
  boolean: Yup.boolean as CustomBooleanSchemaConstructor,
  array: Yup.array as CustomArraySchemaConstructor,
  string: Yup.string as CustomStringSchemaConstructor,
  mixed: Yup.mixed as CustomMixedSchemaConstructor,
  number: Yup.number as CustomNumberSchemaConstructor,
  date: Yup.date as CustomDateSchemaConstructor,
  bigNumber: () => new BigNumberSchema() as CustomBigNumberSchema,
};
// ...

and finally exports the validator function:

// ...

const validator = yupValidator;

export { validator };

Another similar approach but then without the constructors is using a registerValidations function.

Source: https://github.com/hudas/react-study-playground/blob/95809bf405e489050087c2d9bd6d0b10611b2cbb/src/lib/validators/CustomValidatorRegistry.tsx

import * as yup from "yup";
import {inRangeTest} from "./moment/DateRangeValidator";

export function registerValidations() {
  yup.addMethod<yup.MixedSchema>(yup.mixed, 'inRange', function (from, to) {
    return inRangeTest(this)(from, to);
  });
}

// ...

Then in App.js calls this function:

class App extends Component<any, AppState> {
   // ...
   constructor(props: any) {
        super(props);
        registerValidations();
    }
// ...

And I guess then something also has to be done with a validationRegistrar.

These approaches are kinda limiting since they would require you to define the new methods via a global manner. Would rather only define and use them on the location where they're used.

I ended up with this that works so far without losing typing for [email protected]:

import * as yup from "yup";
import { AnyObject, Maybe } from "yup/lib/types";

yup.addMethod<yup.StringSchema>(yup.string, "emptyAsUndefined", function () {
  return this.transform((value) => (value ? value : undefined));
});

yup.addMethod<yup.NumberSchema>(yup.number, "emptyAsUndefined", function () {
  return this.transform((value, originalValue) =>
    String(originalValue)?.trim() ? value : undefined
  );
});

declare module "yup" {
  interface StringSchema<
    TType extends Maybe<string> = string | undefined,
    TContext extends AnyObject = AnyObject,
    TOut extends TType = TType
  > extends yup.BaseSchema<TType, TContext, TOut> {
    emptyAsUndefined(): StringSchema<TType, TContext>;
  }

  interface NumberSchema<
    TType extends Maybe<number> = number | undefined,
    TContext extends AnyObject = AnyObject,
    TOut extends TType = TType
  > extends yup.BaseSchema<TType, TContext, TOut> {
    emptyAsUndefined(): NumberSchema<TType, TContext>;
  }
}

export default yup;

Was this page helpful?
0 / 5 - 0 ratings