21

TypeScript

Use inferred paths and values, name schema types, type wrapper components, and pass forms between components.

NotForm is typed from your schema. In most forms you never write a NotForm type by hand.

What is inferred

const schema = z.object({
  address: z.object({
    city: z.string()
  }),
  email: z.string(),
  tags: z.array(z.string()),
})

const form = useForm({
  initialValues: {
    address: { city: '' },
    email: '',
    tags: []
  },
  schema
})

form.field('email') // FieldInstance<string>
form.field('address.city') // FieldInstance<string>
form.field('tags.0') // FieldInstance<string>
form.arrayField('tags') // ArrayFieldInstance<string>
form.field('adress') // type error: path does not exist
form.arrayField('email') // type error: 'email' is not an array

Inside a slot, field is typed from the path, so field.model has the right type.

Name the schema types

import type { SchemaInput, SchemaOutput } from 'notform'

type FormValues = SchemaInput<typeof schema>
type SubmitData = SchemaOutput<typeof schema>

async function save(data: SubmitData) {
  // ...
}

Use these when a function takes onSubmit data, or when you build initial values elsewhere:

const initialValues: FormValues = {
  address: { city: '' },
  email: '',
  tags: []
}

initialValues accepts a deep-partial of the input, so you can start with only some keys. Field paths still come from the full input shape.

Path helpers

TypeMeaning
InputPath<TSchema>Every valid path into the input. Accepted by form.field() and form.setValue().
InputArrayPath<TSchema>Paths that point at an array. Accepted by form.arrayField().
InputPathValue<TSchema, TPath>The input type at a path.
OutputPath<TSchema> / OutputPathValue<TSchema, TPath>The same, for the output shape.
ArrayItem<TArray>The item type of an array type.
import type { InputPath } from 'notform'

type FieldName = InputPath<typeof schema>

Path types recurse to a depth of 10. Forms nested deeper than that lose path autocomplete beyond it.

Type a reusable input component

A wrapper takes a field instance as a prop.

<script setup lang="ts">
import { FieldError, type FieldInstance } from 'notform'

defineProps<{
  field: FieldInstance<string>
  label: string
}>()
</script>

<template>
  <div>
    <label :for="field.path">{{ label }}</label>

    <input
      :id="field.path"
      v-model="field.model"
      v-bind="field.events"
    >

    <FieldError :errors="field" />
  </div>
</template>
<template>
  <TextField
    :field="form.field('email')"
    label="Email"
  />
</template>

Array items are fields too, so ArrayItemInstance<T> is also accepted where a FieldInstance<T> is expected.

If your linter reports vue/no-mutating-props for v-model="field.model", it is a false positive. A field instance is a reactive object designed to be written through, and you are not replacing the prop.

Pass a form to a child component

Type the prop with UseFormInstance, using the schema's type.

<script setup lang="ts">
import type { UseFormInstance } from 'notform'
import type { profileSchema } from './profile-schema'

defineProps<{ form: UseFormInstance<typeof profileSchema> }>()
</script>

<template>
  <button
    type="submit"
    :disabled="form.isSubmitting || !form.isDirty"
  >
    Save
  </button>

  <button
    type="button"
    @click="form.reset()"
  >
    Discard
  </button>
</template>
<template>
  <Form :form="form">
    <FormActions :form="form" />
  </Form>
</template>

Put the schema in its own file so both components can import its type. Prefer UseFormInstance over FormInstance. It is the public contract and excludes methods meant to stay internal.

Schema requirements

useForm accepts a Standard Schema whose input and output are objects. This is what lets paths and form.values be keyed. A schema such as z.string() fails type checking.

Next steps

Copyright © 2026 NotForm
Published under MIT License