TypeScript
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
| Type | Meaning |
|---|---|
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.
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
- Types reference lists every exported type.