Closed10

フォームライブラリとバリデーションライブラリのキャスト

君津君津

Formik + Yup

数値での確認

Yup

test()でとれるvalue, ctx.parentはキャスト後
空の場合はundefined

Formik

formik.values, onSubmitvalueはキャスト前
空の場合は''
キャストするとエラーが投げられる

君津君津

React Hook Form + Yup

Yup

キャスト後
空の場合はNaN

React Hook Form

field.value, handleSubmitvalueはキャスト後
空の場合はフォームがinvalidになる

君津君津
yup.addMethod(yup.number, "optional", function (this) {
  return this.transform((value, originalValue) => {
    return originalValue === "" ? undefined : value;
  });
});

を追加してoptional()を利用するようにすると空の場合のキャスト後の値をundefinedにできて丸く収まりそう

君津君津

日付の場合(Yup + MUI + Day.js)

日付の場合は少し複雑
DateSchemaを利用する場合、transformstringを返すことはできないが、普通はリクエスト時に文字列にする
なのでMixedSchematransformするかな
そうなるとDateで値を持っておく必要もないのでDayjsのまま保持するのが良さそう

// yup.mixed<dayjs.Dayjs>().optionalDate()のように使う
declare module "yup" {
  interface MixedSchema {
    optionalDate(): this;
  }
}
yup.addMethod(yup.mixed, "optionalDate", function (this) {
  return this.transform((value) => {
    return value === null ? undefined : value.format("YYYY-MM-DD");
  });
});

Formik

DatePickerのonChangeではイベントではなく値だけがくるのでsetFieldValueする必要

<DatePicker
  label="date1"
  value={formik.values.date1}
  onChange={(date) => formik.setFieldValue("date1", date)}
  slotProps={{ field: { clearable: true } }}
/>
君津君津

React Hook Form + Yupのとき、React Hook Form側でキャストされているのはyupResolverの設定だった

  • キャストしたい時:デフォルト
  • キャストしたくない時:yupResolver(schema, {}, { raw: true })

Yup側でキャストしたくない時は第2引数で{ strict: true }を指定するけどこれは使わないかな

君津君津
declare module "yup" {
  interface MixedSchema {
    optionalDate(): this;
    requiredDate(): this;
    minDate(minDate: dayjs.Dayjs): this;
    maxDate(maxDate: dayjs.Dayjs): this;
  }
}
yup.addMethod(yup.mixed, "optionalDate", function (this) {
  return this.transform((value) => {
    if (!dayjs.isDayjs(value)) return undefined;
    return value.format("YYYY-MM-DD");
  });
});
yup.addMethod(yup.mixed, "requiredDate", function (this) {
  return this.optionalDate().required();
});
yup.addMethod(yup.mixed, "minDate", function (this, minDate: dayjs.Dayjs) {
  return this.test("minDate", "minDate", (value) => {
    if (value === undefined) return true;
    if (typeof value !== "string") return false;
    const d = dayjs(value);
    return d.isAfter(minDate, "date") || d.isSame(minDate, "date");
  });
});
yup.addMethod(yup.mixed, "maxDate", function (this, maxDate: dayjs.Dayjs) {
  return this.test("maxDate", "maxDate", (value) => {
    if (value === undefined) return true;
    if (typeof value !== "string") return false;
    const d = dayjs(value);
    return d.isBefore(maxDate, "date") || d.isSame(maxDate, "date");
  });
});
君津君津

Zod

Zodはpreprocessvalidationtransformの順に行われるよう

valueAs.ts
export const asNumber = (arg: unknown): number | undefined => {
  if (arg === "") {
    return undefined;
  }
  return Number(arg);
};

export const asDate = (arg: unknown): Date | undefined => {
  if (dayjs.isDayjs(arg)) {
    return arg.toDate();
  }
  if (typeof arg === "string") {
    return new Date(arg);
  }
  return undefined;
};
zodSchema.ts
export const schema = z.object({
  num: z.preprocess(asNumber, z.number().optional()),
  date: z
    .preprocess(asDate, z.date().optional())
    .transform((value) => value?.toJSON()),
});
このスクラップは3ヶ月前にクローズされました