UForm state type is incorrectly constrained to schema input (breaks nullable fields, other mixed-type form state)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 6.9k
- Forks
- 1.1k
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 57
Description
Environment
- Operating System: Darwin
- Node Version: v24.5.0
- Nuxt Version: 4.1.2
- CLI Version: 3.28.0
- Nitro Version: 2.12.5
- Package Manager: pnpm@10.15.0
- Builder: -
- User Config: compatibilityDate, ssr, alias, typescript, nitro, vite, modules, i18n, css, hooks, icon, runtimeConfig, $development, $test
- Runtime Modules: @nuxt/icon@2.0.0, @nuxt/ui@4.0.0, @nuxtjs/i18n@10.1.0, nuxt-auth-utils@0.5.24
- Build Modules: -
Is this bug related to Nuxt or Vue?
Nuxt
Package
v4.x
Version
4.0.0
Reproduction
https://codesandbox.io/p/devbox/epic-shamir-l5ccfm
Description
The code as simple as this:
<script setup lang="ts">
import * as z from "zod"
import { reactive } from "vue"
const fields = reactive({
// In UI, the file may be selected or not.
file: null as File | null,
})
const schema = z.object({
// But the form is only considered valid when the file is selected.
file: z.file(),
})
</script>
<template>
<u-form :state="fields" :schema="schema">
<u-file-upload v-model="fields.file" />
</u-form>
</template>
Triggers type error:
Type '{ file: { readonly lastModified: number; readonly name: string; readonly webkitRelativePath: string; readonly size: number; readonly type: string; arrayBuffer: () => Promise<ArrayBuffer>; slice: (start?: number | undefined, end?: number | undefined, contentType?: string | undefined) => Blob; stream: () => ReadableSt...' is not assignable to type 'Partial<{ file: File; }>'
Expected: it's allowed to have a form with a schema that requires a file to be selected.
Additional context
In fact, I believe this is a much broader problem. Why is the state typed as Partial<InferInput<S>> in the first place? The type of the fields does not always match the schema. Basically, that's the point of schema in the first place, to perform validation — and not only value, but also the type. Consider number input value:
<script setup lang="ts">
import * as z from "zod"
const fields = reactive({
// Type used by <input v-model="..." type="number">
// It saves empty string for non-numbers.
foo: "" as number | "",
})
const schema = z.object({
// But we need the actual number for the input data.
foo: z.number(),
})
</script>
<template>
<u-form :state="fields" :schema="schema">
<input v-model="fields.foo" type="number" />
</u-form>
</template>
This leads to similar type error:
Type '{ foo: number | ""; }' is not assignable to type 'Partial<{ foo: number; }>'.
Types of property 'foo' are incompatible.
Type 'number | ""' is not assignable to type 'number | undefined'.
Type 'string' is not assignable to type 'number'.
As I see it, state should be untyped/unknown.
Logs
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the UForm state prop typing and schema generic, using the linked CodeSandbox reproduction to compare nullable File state and number-or-empty-string state against their schemas. Done means valid forms no longer reject state whose input types differ from the schema output while schema validation still works for both examples.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nuxt, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100