modular-forms icon indicating copy to clipboard operation
modular-forms copied to clipboard

[Qwik] Type checking for forms with variants

Open ianlet opened this issue 6 months ago • 1 comments

Let's say I have the following schema with multiple variants requiring different fields based on the placeType field.

Here's the simplified version so I can better refer to it below:

const BaseSchema = v.object({
  name: v.pipe(v.string(), v.nonEmpty()),
  coordinates: v.object({
    latitude: v.number(),
    longitude: v.number(),
  }),
  thumbnailUrl: v.optional(v.pipe(v.string(), v.url())),
  websiteUrl: v.optional(v.pipe(v.string(), v.url())),
});

const FeaturesSchema = v.object({
  features: v.pipe(v.array(v.picklist(PlaceFeatures)), v.minLength(1)),
});

const DescriptionSchema = v.object({
  description: v.optional(v.string()),
});

const AddActivitySchema = v.object({
  ...BaseSchema.entries,
  ...FeaturesSchema.entries,
});

const AddDestinationSchema = v.object({
  ...BaseSchema.entries,
  ...FeaturesSchema.entries,
  ...DescriptionSchema.entries,
});


export const AddPlaceSchema = v.variant("placeType", [
  v.object({
    placeType: v.literal("activity"),
    ...AddActivitySchema.entries,
  }),
  v.object({
    placeType: v.literal("destination"),
    ...AddDestinationSchema.entries,
  }),
  v.object({
    placeType: v.picklist(PlaceTypes),
    ...BaseSchema.entries,
  }),
]);

export type AddPlaceForm = v.InferInput<typeof AddPlaceSchema>;

I want to use this schema to build a form in Qwik, but I'm having difficulties taming the Typescript compiler because it's always inferring that features is an undefined field or never.

Here's a minimal example to illustrate this issue:

const AddPlaceForm = component$(
  () => {
    const [addPlaceForm, { Form, Field }] = useForm<AddPlaceForm>({
      loader: {
        value: {
          name: "",
          thumbnailUrl: undefined,
          websiteUrl: undefined,
          coordinates: placeCoordinates,
          placeType: undefined,
          features: [],
          description: undefined,
        },
      },
      action: useAddPlaceAction(),
      validate: valiForm$(AddPlaceSchema),
    });

    const placeType = useComputed$(() => getValue(addPlaceForm, "placeType"));

    return (
      <Form>
           {placeType.value === "activity" && (
             <>
               {placeFeatures.value.map((feature) => (
                 <Field
                   name="features"
                   type="string[]"
                   key={`feature-${feature}`}
                 >
                   {(field, props) => (
                     <Checkbox
                       {...props}
                       value={feature}
                       checked={field.value?.includes(feature)}
                     >
                       {feature}
                     </Checkbox>
                   )}
                 </Field>
               ))}
             </>
           )}
      </Form>
    );
});

Which will yield the following errors:

src/components/map/places/add-place-modal.tsx:218:23 - error TS2322: Type 'string' is not assignable to type 'undefined'.

218                       type="string[]"
                          ~~~~

  ../../node_modules/@modular-forms/qwik/dist/types/components/Field.d.ts:20:5
    20     type: FieldType<FieldPathValue<TFieldValues, TFieldName>>;
           ~~~~
    The expected type comes from property 'type' which is declared here on type 'IntrinsicAttributes & Pick<Partial<Omit<FieldProps<{ name: string; coordinates: { latitude: number; longitude: number; }; features: ("workspace" | "skiing" | "snowshoeing" | "nordic-skiing" | ... 8 more ... | "bathroom")[]; placeType: "activity"; link?: string | undefined; th...'

src/components/map/places/add-place-modal.tsx:225:49 - error TS2339: Property 'includes' does not exist on type 'never'.

225                           checked={field.value?.includes(feature)}

Do you have an (elegant) recommendation on how to satisfy the compiler and let it know that we want a specific variant of the form in that case?

ianlet avatar Aug 23 '24 18:08 ianlet