Skip to content

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

Description

@IlyaSemenov

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    triageAwaiting initial review and prioritization

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions