nuxt / nuxt/ui

UForm state type is incorrectly constrained to schema input (breaks nullable fields, other mixed-type form state)

Open
#5,204 6 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

triage
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.