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

Type Error `errorBag` using array in form

Open Yuki992411 opened this issue 1 year ago • 2 comments

What happened?

When I use errorBag in useForm with array format, type-check (vue-tsc --noEmit) is failed. So when the following code is executed, it points out that the type definition at point errorBag[`timeCandidates[${index}].time`] and errorBag[`timeCandidates[${index}].num`] are incorrect.

Sample Code
<template>
  <div>
    <h1>This is an form page</h1>
    <form @submit="onSubmit">
      <div>
        <label>Candidates</label>
        <ul v-for="(_, index) in fields" :key="`timeCandidates_${index}`">
          <li>
            <div class="candidate">
              <Field
                :id="`timeCandidates_${index}_time`"
                :name="`timeCandidates[${index}].time`"
                v-slot="{ field, handleBlur }"
              >
                <input v-bind="field" @blur="handleBlur" class="input" />
              </Field>
              <Field
                :id="`timeCandidates_${index}_num`"
                :name="`timeCandidates[${index}].num`"
                v-slot="{ field, handleBlur }"
              >
                <input v-bind="field" @blur="handleBlur" class="input" type="number" />
              </Field>
              <div @click="remove(index)">delete</div>
            </div>
            <div
              class="errors"
              v-for="error in errorBag[`timeCandidates[${index}].time`]"
              :key="`timeCandidates_${index}_time_${error}`"
            >
              {{ error }}
            </div>
            <div
              class="errors"
              v-for="error in errorBag[`timeCandidates[${index}].num`]"
              :key="`timeCandidates_${index}_num_${error}`"
            >
              {{ error }}
            </div>
          </li>
        </ul>
        <div @click="push(initialTimeCandidate)">add</div>
      </div>
      <button :disabled="!meta.valid">Submit</button>
    </form>
  </div>
</template>

<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/zod'
import { Field, useFieldArray, useForm } from 'vee-validate'
import { watch } from 'vue'
import { z } from 'zod'

const FormSchema = z.object({
  timeCandidates: z.array(
    z.object({
      time: z.string().regex(/^[0-9]{2}:[0-9]{2}$/, {
        message: 'input hh:mm'
      }),
      num: z.coerce.number().min(3)
    })
  )
})

const initialTimeCandidate = { time: '', num: 0 } as const satisfies z.infer<
  typeof FormSchema
>['timeCandidates'][number]

const { meta, errorBag, handleSubmit } = useForm<z.infer<typeof FormSchema>>({
  validationSchema: toTypedSchema(FormSchema),
  initialValues: {
    timeCandidates: [initialTimeCandidate]
  }
})

const { push, remove, fields } =
  useFieldArray<z.infer<typeof FormSchema>['timeCandidates'][number]>('timeCandidates')

const onSubmit = handleSubmit((values) => {
  console.log(values)
  alert(JSON.stringify(values, null, 2))
})

watch(errorBag, (value) => {
  console.log('[debug] errorBag: ', value)
})
</script>

<style>
.input {
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 8px;
  margin: 8px;
}

.candidate {
  display: flex;
  gap: 8px;
  align-items: center;
}

.errors {
  color: red;
}
</style>

Type-check output vue-tsc --build --force

src/views/FormView.vue:28:31 - error TS7053: Element implicitly has an 'any' type because expression of type '`timeCandidates[${number}].time`' can't be used to index type 'Partial<Record<"timeCandidates" | `timeCandidates.${number}` | `timeCandidates.${number}.time` | `timeCandidates.${number}.num`, string[]>>'.

28               v-for="error in errorBag[`timeCandidates[${index}].time`]"
                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/views/FormView.vue:35:31 - error TS7053: Element implicitly has an 'any' type because expression of type '`timeCandidates[${number}].num`' can't be used to index type 'Partial<Record<"timeCandidates" | `timeCandidates.${number}` | `timeCandidates.${number}.time` | `timeCandidates.${number}.num`, string[]>>'.

35               v-for="error in errorBag[`timeCandidates[${index}].num`]"

However, on runtime, there is no problem.

スクリーンショット 2024-06-02 6 34 43

When the object is checked, it stores a value different from the type definition.

スクリーンショット 2024-06-02 6 35 52

So, even if you modify it according to type-check, the validation error messages will not be displayed correctly on the runtime.

- v-for="error in errorBag[`timeCandidates[${index}].time`]"
+ v-for="error in errorBag[`timeCandidates.${index}.time`]"
-  v-for="error in errorBag[`timeCandidates[${index}].num`]"
+ v-for="error in errorBag[`timeCandidates.${index}.num`]"

I think the problem is in the PathInternal type definition.

Reproduction steps

...

Version

Vue.js 3.x and vee-validate 4.x

What browsers are you seeing the problem on?

  • [ ] Firefox
  • [X] Chrome
  • [ ] Safari
  • [ ] Microsoft Edge

Relevant log output

No response

Demo link

https://stackblitz.com/edit/vee-validate-issue-template-nsbdfw?file=src%2FApp.vue

When you try type-check in terminal, you can see error.

vue-tsc --noEmit

N/A

Code of Conduct

  • [X] I agree to follow this project's Code of Conduct

Yuki992411 avatar Jun 01 '24 21:06 Yuki992411

Any update on this?

ghimisradu avatar Aug 23 '24 08:08 ghimisradu

I added StackBlitz to the issue summary. So, you can check this.

Yuki992411 avatar Aug 25 '24 23:08 Yuki992411

This is a tough issue to fix, because we have 2 path syntaxes that get confused.

Closing this in favor of tracking it in #4295, this is probably a v5 change that will be breaking since we are more likely to drop using [] rather than support it everywhere.

logaretm avatar Oct 19 '24 17:10 logaretm