---
title: "TypeScript"
description: "Use inferred paths and values, name schema types, type wrapper components, and pass forms between components."
canonical_url: "https://notformdocs.vercel.app/docs/guides/typescript"
---

> Index of every page on this site: [llms.txt](https://notformdocs.vercel.app/llms.txt)

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

## What is inferred

```ts
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

```ts
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:

```ts
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

<table>
<thead>
  <tr>
    <th>
      Type
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        InputPath<TSchema>
      </code>
    </td>
    
    <td>
      Every valid path into the input. Accepted by <code>
        form.field()
      </code>
      
       and <code>
        form.setValue()
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        InputArrayPath<TSchema>
      </code>
    </td>
    
    <td>
      Paths that point at an array. Accepted by <code>
        form.arrayField()
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        InputPathValue<TSchema, TPath>
      </code>
    </td>
    
    <td>
      The input type at a path.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        OutputPath<TSchema>
      </code>
      
       / <code>
        OutputPathValue<TSchema, TPath>
      </code>
    </td>
    
    <td>
      The same, for the output shape.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ArrayItem<TArray>
      </code>
    </td>
    
    <td>
      The item type of an array type.
    </td>
  </tr>
</tbody>
</table>

```ts
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.

<code-collapse>

```vue
<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>
```

</code-collapse>

```vue
<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.

<note>

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.

</note>

## Pass a form to a child component

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

```vue
<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>
```

```vue
<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

- [Types reference](https://notformdocs.vercel.app/docs/reference/types) lists every exported type.

## Sitemap

See the full [sitemap](https://notformdocs.vercel.app/sitemap.md) for all pages.
