fabian-hiller / modular-forms

The modular and type-safe form library for SolidJS, Qwik and Preact
https://modularforms.dev
MIT License
1.05k stars 55 forks source link

[Qwik] Type checking for forms with variants #242

Open ianlet opened 2 months ago

ianlet commented 2 months ago

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?

fabian-hiller commented 2 months ago

Yes, this is a problem with the current implementation of Modular Forms, which I will fix when I rewrite the library. To satisfy the compiler, you could add features: v.undefined() wherever features is not needed. The initial object structure for .loader is needed to initialize the form store.