Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Types of fields are invalid when type of validationSchema is a union #4925

Open
1 task done
rijenkii opened this issue Nov 3, 2024 · 5 comments
Open
1 task done
Labels
✨ enhancement a "nice to have" feature 👕 TypeScript TypeScript typings issue

Comments

@rijenkii
Copy link
Author

rijenkii commented Nov 3, 2024

Update: seems like types of everything that uses field names only uses them from the first schema:
image
image

With this revelation, workaround now looks like this:
image
playground

@rijenkii rijenkii changed the title Type of values in submission handler is invalid when type of validationSchema is a union Types of fields are invalid when type of validationSchema is a union Nov 3, 2024
@rijenkii rijenkii changed the title Types of fields are invalid when type of validationSchema is a union Types of fields are invalid when validationSchema is an array Nov 3, 2024
@rijenkii rijenkii changed the title Types of fields are invalid when validationSchema is an array Types of fields are invalid when type of validationSchema is a union Nov 4, 2024
@logaretm
Copy link
Owner

logaretm commented Nov 7, 2024

Yea, useForm is not optimized to handle multiple schema types or multistep forms for that matter.

I would think we first need to merge the schemas type-wise, and then infer the path types. I think this is better done in a useMultistepForm or something similar since we would be in a better position to do that reliably.

But I'm not planning on working on a similar API at the moment. Do you think there is a possible workaround we can recommend in the docs for that situation?

@logaretm logaretm added ✨ enhancement a "nice to have" feature 👕 TypeScript TypeScript typings issue labels Nov 7, 2024
@rijenkii
Copy link
Author

rijenkii commented Nov 8, 2024

Do you think there is a possible workaround we can recommend in the docs for that situation?

The workaround I'm using is demonstrated in this playground, it is not really trivial though.
The main part is basically casting currentSchema to ComputedRef<FixedSchema> before passing it into the useForm.

EDIT:
If in the mood for even more shenanigans, it is possible to even restrict the type of currentStep, to ease the unsafetyness of ! in the currentSchema getter:

const schemas = [...] as const;  // as const is important.

type Enumerate<N extends number, Acc extends number[] = []> = Acc["length"] extends N
  ? Acc[number]
  : Enumerate<N, [...Acc, Acc["length"]]>;

type ArrayIdx<T extends readonly unknown[]> = number extends T["length"]
  ? number
  : Enumerate<T["length"]>;

const currentStep = ref<ArrayIdx<typeof schemas>>(0);
//    ^? const currentStep: Ref<0 | 1, 0 | 1>

But I think this is too much for a simple workaround.

@logaretm
Copy link
Owner

logaretm commented Nov 8, 2024

You are right, this is too complex to be in the docs. We could expose this as a utility type if possible to make it easier for people to use this. But the DX isn't great here 🤔

@rijenkii
Copy link
Author

rijenkii commented Nov 11, 2024

Not enough time to come up with a full-fledged useMultistepForm, but managed to make a useMultistepSchema:

import type { ArrayIndices, UnionToIntersection } from "type-fest";
import type { InferInput, InferOutput, TypedSchema } from "vee-validate";
import type { Ref } from "vue";
import { computed, readonly, ref, toValue } from "vue";
import type { z } from "zod";

type AnyRef<T> = Parameters<typeof toValue<T>>[0];
type ToValue<T> = T extends AnyRef<infer U> ? U : never;

type RawSchema<T = unknown, V = unknown> = AnyRef<TypedSchema<T, V>>;
type FixedValues<T extends RawSchema> = UnionToIntersection<InferInput<ToValue<T>>>;
type FixedOutput<T extends RawSchema> = UnionToIntersection<InferOutput<ToValue<T>>>;
type FixedSchema<T extends RawSchema> = TypedSchema<FixedValues<T>, FixedOutput<T>>;

export function useMultistepSchema<
  RawValues extends Record<string, unknown>,
  RawOutput extends Record<string, unknown>,
  RawSchemas extends readonly [
    RawSchema<RawValues, RawOutput>,
    ...RawSchema<RawValues, RawOutput>[],
  ],
>(schemas: RawSchemas) {
  const currentStep = ref(0);
  const currentSchema = computed(() => toValue(schemas[currentStep.value]));

  return {
    currentSchema: readonly(currentSchema as Ref<FixedSchema<RawSchemas[number]>>),
    currentStep: currentStep as Ref<ArrayIndices<RawSchemas>>,
  };
}

Usage:

const { currentStep, currentSchema } = useMultistepSchema([
  computed(() =>
    toTypedSchema(
      z.object({
        field1: z.string(),
        field2: z.string(),
      }),
    ),
  ),
  toTypedSchema(
    z.object({
      field3: z.string(),
      field4: z.string(),
    }),
  ),
]);

const { handleSubmit, values } = useForm({
  validationSchema: currentSchema,
  keepValuesOnUnmount: true,
  initialValues: { field4: "" },
});

handleSubmit((values) => {
  if (currentStep.value === 0) return (currentStep.value = 1);
  console.log(values);
});

playground


Additional note about zod: as per #4284 (comment), you need to make all your schemas .passthrough, else zod will strip all unknown keys on validate. However, because of that typescript now thinks that values actually has all of the unknown fields:

(parameter) values: {
    field1: string;
    field2: string;
} & {
    [k: string]: unknown;
} & {
    field3: string;
    field4: string;
}
setFieldError(field: string, message: string | string[] | undefined): void

To combat this, the schema needs to be made passthrough silently, and typescript need not be informed about it:

import { toTypedSchema as _toTypedSchema } from "@vee-validate/zod";

function toTypedSchema<T extends z.ZodObject<z.ZodRawShape, "strip">>(
  schema: T,
) {
  return _toTypedSchema(schema.passthrough() as unknown as T);
}

After that all unknown fields are missing, and types of functions like setFieldError receive only valid field names: playground:

(parameter) values: {
    field1: string;
    field2: string;
} & {
    field3: string;
    field4: string;
}
setFieldError(field: "field1" | "field2" | "field3" | "field4", message: string | string[] | undefined): void

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✨ enhancement a "nice to have" feature 👕 TypeScript TypeScript typings issue
Projects
None yet
Development

No branches or pull requests

2 participants