Продвинутая схема
Композиция схем
import { mergeSchemas, omitFields, pickFields, extendField } from '@macrulez/vue-form-schema'
const base = [
{ type: 'text' as const, name: 'firstName' },
{ type: 'text' as const, name: 'lastName' },
{ type: 'email' as const, name: 'email' },
]
// Объединение — при совпадении имени побеждает более поздняя схема
const extended = mergeSchemas(base, [{ type: 'text' as const, name: 'phone' }])
// Удаление полей
const noEmail = omitFields(base, ['email'])
// Оставить только конкретные поля
const nameOnly = pickFields(base, ['firstName', 'lastName'])
// Немутирующий патч
const required = extendField(base, 'email', { required: true, label: 'Email address' })Дискриминированные схемы
Распространённый паттерн: весь набор полей меняется в зависимости от значения одного поля-«дискриминатора» — способ оплаты, тип адреса, тип документа. Связывать visible вручную для каждого поля — многословно и легко ошибиться. discriminatedFields(discriminatorName, variants) строит эту visible-связку за вас:
import { discriminatedFields } from '@macrulez/vue-form-schema'
import { useForm } from '@macrulez/vue-form-schema'
const schema = [
{
type: 'radio' as const,
name: 'paymentMethod',
label: 'Payment method',
options: [
{ label: 'Card', value: 'card' },
{ label: 'PayPal', value: 'paypal' },
],
},
...discriminatedFields('paymentMethod', {
card: [
{ type: 'text' as const, name: 'cardNumber', label: 'Card number', required: true },
{ type: 'text' as const, name: 'cvc', label: 'CVC', required: true },
],
paypal: [
{ type: 'email' as const, name: 'paypalEmail', label: 'PayPal email', required: true },
],
}),
]
const { fields } = useForm({ schema, clearOnHide: true })discriminatedFields не создаёт само поле-дискриминатор — определите его отдельно (обычно select/radio) и распылите результат хелпера рядом с ним. visible каждого возвращённого поля устанавливается как «значение дискриминатора совпадает с этим вариантом», объединённое через AND с собственным visible поля, если оно уже было. Комбинируйте с clearOnHide: true в useForm, чтобы при переключении варианта сбрасывались значения теперь скрытого варианта.
Нативный маппинг из Zod / Valibot: parseZod принимает корневую схему z.discriminatedUnion(key, [...]), а parseValibot принимает корневую схему v.variant(key, [...]) — оба конвертируются прямо в select-дискриминатор плюс поля вариантов, связанные через discriminatedFields:
import { z } from 'zod'
import { parseZod } from '@macrulez/vue-form-schema/zod'
const schema = z.discriminatedUnion('paymentMethod', [
z.object({ paymentMethod: z.literal('card'), cardNumber: z.string(), cvc: z.string() }),
z.object({ paymentMethod: z.literal('paypal'), paypalEmail: z.string().email() }),
])
const fields = parseZod(schema)Это применяется только тогда, когда дискриминированное объединение/вариант является корневой схемой, переданной в parseZod/parseValibot — дискриминированное объединение, вложенное как свойство внутри более крупного z.object({...}), не разворачивается автоматически; для этого случая используйте discriminatedFields напрямую.
Вывод типов TypeScript
Из схемы Zod / Yup / Valibot (автоматически)
parseZod / parseYup / parseValibot возвращают FieldDefinition[], который также несёт выведенный тип значений исходной схемы. useForm({ schema: fields }) подхватывает его автоматически — явный useForm<Values>(...) не нужен:
import { z } from 'zod'
import { parseZod } from '@macrulez/vue-form-schema/zod'
import { useForm } from '@macrulez/vue-form-schema'
const schema = z.object({ username: z.string(), age: z.number() })
const fields = parseZod(schema)
const { values } = useForm({
schema: fields,
onSubmit: (data) => {
data.username // string ✓ — выведено из `schema`, а не Record<string, unknown>
},
})Это работает так же для parseYup (через InferType из Yup) и parseValibot (через InferOutput из Valibot). Явный useForm<Values>({ schema: fields }) по-прежнему работает и переопределяет выведенный тип, если он вам нужен.
Из написанного вручную FieldDefinition[]
InferValues<T> отображает литерал readonly FieldDefinition[] в типизированный объект значений — используйте это, когда схема не приходит из Zod/Yup/Valibot.
import { defineSchema } from '@macrulez/vue-form-schema'
import type { InferValues } from '@macrulez/vue-form-schema'
const schema = defineSchema([
{ type: 'text' as const, name: 'username' as const },
{ type: 'number' as const, name: 'age' as const },
{ type: 'checkbox' as const, name: 'agreed' as const },
] as const)
type Values = InferValues<typeof schema>
// { username: string; age: number; agreed: boolean }
const { values } = useForm<Values>({ schema })
// values.value.username — string ✓Маппинг типов: checkbox → boolean, number → number, array → unknown[], всё остальное → string.