vee-validate icon indicating copy to clipboard operation
vee-validate copied to clipboard

Infer model type from schema when using defineField()

Open bechtold opened this issue 1 year ago • 4 comments

Is your feature request related to a problem? Please describe.

I'm using typescript, yup, toTypedSchema(), and it works like a charm after getting used to values from useForm() still having | undefined types, but values from onSubmit() having the required type.

The problem: the model that is returned from defineField() has type Ref<unknown>. Screenshot 2024-07-08 at 13 02 57

This is annoying when assigning it to components typed model: Screenshot 2024-07-08 at 13 03 12

Casting the type when passing it to the component works, but is not nice I guess. Especially if you use the model multiple times, you don't want to cast it every time. Also, my understanding is, that casting would not bring the benefits that I'm looking for from typescript. Screenshot 2024-07-08 at 13 13 25

The same goes for using the ref in code: Screenshot 2024-07-08 at 13 18 44

Similar when defining, for example, a boolean and using it in a v-if. It would be possible to use values.variableName, but why not use the model?

Especially because the type is defined in the schema: Screenshot 2024-07-08 at 13 05 10

After fiddling around a bit, I found out that adding the field's path to the generic type of defineField() does help to infer the proper type. Screenshot 2024-07-08 at 13 03 39

Also in code the ref's type is inferred correctly: Screenshot 2024-07-08 at 13 23 28

Describe the solution you'd like

It would be nice if the model's type is inferred automatically instead of manually having to do it. Maybe I'm missing something, so I'm grateful for any hints.

Describe alternatives you've considered

Passing the field's path to infer the type from helps, but is redundant: defineField<"pathToField">("pathToField")

bechtold avatar Jul 08 '24 11:07 bechtold

it is possible that it is getting confused by the MaybeRefOrGetter thrown in there. I didn't encounter this in the tests nor in my daily usage of this.

Can you create a minimal reproduction for this?

logaretm avatar Jul 18 '24 23:07 logaretm

@logaretm Probably this is happening because of incorrect typed vuetifyConfig (we can see on the screenshot author is using it)

import * as yup from 'yup';
import { useForm, type LazyInputBindsConfig } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/yup';

const FormSchema = yup.object({
  fieldOne: yup.number().required(),
  fieldTwo: yup.number().required(),
});

const { defineField } = useForm({
  validationSchema: toTypedSchema(FormSchema),
});

const vuetifyBindsConfig: LazyInputBindsConfig = (state) => ({
  props: {
    'error-messages': state.errors,
  },
});

const [fieldOne] = defineField('fieldOne', vuetifyBindsConfig); // unknow
const [fieldTwo] = defineField('fieldTwo'); // number | undefined

// or
const [fieldOne] = defineField<'fieldOne'>('fieldOne', vuetifyBindsConfig); // number | undefined

Problem is i don't know correct type for vuetifyBindsConfig. :)

zumm avatar Jul 30 '24 18:07 zumm

And there is another issue with defineField typing. It defines type of object fields as PartialObjectDeep<T, {}> | undefined so they can't be used with components which expect T as model.

import * as yup from 'yup';
import { useForm from 'vee-validate';
import { toTypedSchema } from '@vee-validate/yup';

const FormSchema = yup.object({
  objectField: yup.object({
    id: yup.string().required()
    // ...
  })
});

const { defineField } = useForm({
  validationSchema: toTypedSchema(FormSchema),
});

const [objectField] = defineField('objectField');

// type error:
// <SomeFieldComponent v-model="objectField" />

// SomeFieldComponent.vue
defineModel<{
  id: string
  // ...
}>()

zumm avatar Jul 30 '24 18:07 zumm

Another problem with type inference is when we use defineField with a second argument as a function and that function is not written inline, but extracted to a const or a function.

For example, this works fine (more or less) - the type of title const is any:

const [title, titleAttrs] = defineField('title', (state) => ({
  validateOnModelUpdate: state.touched,
}))

But this doesn't - the type of title const is unknown:

const conf = <T,>(state: PublicPathState<T>) => ({
  validateOnModelUpdate: state.touched,
})
const [title, titleAttrs] = defineField('title', conf)

As a workaround we can skip declaration of state type in the const conf:

const conf = (state) => ({
  validateOnModelUpdate: state.touched,
})
const [title, titleAttrs] = defineField('title', conf)

But this obivously not a best solution, as state has any type and intellisense doesn't work. Linting tools also don't like this:

Parameter 'state' implicitly has an 'any' type.ts(7006)

A workaround from the first post (adding the field's path to the generic type of defineField()) works the best for now:

const conf = <T,>(state: PublicPathState<T>) => ({
  validateOnModelUpdate: state.touched,
})
const [title, titleAttrs] = defineField<'title'>('title', conf)

I would like to add that re-using the second argument of defineField is important feature as it allows to define some "global" rules, that makes sense to the whole project (not just one field). Like the example above, that makes validation lazy until field is touched, and then eager. This type of logic is used in most well-known webapps and I think it works the best.

xak2000 avatar Feb 11 '25 01:02 xak2000