Skip to content

Продвинутая схема

Композиция схем

ts
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-связку за вас:

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

ts
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>(...) не нужен:

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

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

Маппинг типов: checkboxboolean, numbernumber, arrayunknown[], всё остальное → string.