在現代 Web 應用程序中,處理複雜的表單場景需要強大的驗證工具和類型系統。本文將深入探討如何結合 Vee-Validate 和 Zod 的進階特性,以及 TypeScript 的高級類型系統,來創建一個強大、靈活且類型安全的表單處理系統。我們將特別關注 Zod 的高級用法和 TypeScript 的進階特性,展示如何將它們無縫整合到 Vue 3 和 Vee-Validate 中。
首先,讓我們深入探討 Zod 的一些進階特性:
(檔案 : src/schemas/advanceUser.ts
)
// src/schemas/advanced-user.ts
import * as zod from 'zod'
// 使用 nativeEnum 定義角色
enum UserRole {
Admin = 'ADMIN',
User = 'USER',
Guest = 'GUEST'
}
// 基本用戶信息 schema
const baseUserSchema = zod.object({
username: zod.string().min(3).max(20),
email: zod.string().email(),
})
// 使用 extend 擴展基本 schema
const extendedUserSchema = baseUserSchema.extend({
age: zod.number().min(18).max(100),
})
// 使用 merge 合併 schema
const mergedUserSchema = baseUserSchema.merge(zod.object({
birthDate: zod.string().transform((str) => new Date(str)),
}))
// 使用 pick 和 omit
const loginSchema = baseUserSchema.pick({ email: true }).extend({
password: zod.string().min(8),
})
const publicUserSchema = extendedUserSchema.omit({ email: true })
// 使用 nativeEnum 和 enum 的差異
const roleSchema = zod.object({
nativeEnumRole: zod.nativeEnum(UserRole),
stringEnumRole: zod.enum(['ADMIN', 'USER', 'GUEST']),
})
// 使用 transform 和 coerce
const advancedUserSchema = extendedUserSchema.extend({
role: zod.nativeEnum(UserRole),
registrationDate: zod.string().transform((str) => new Date(str)),
age: zod.coerce.number().min(18), // 自動轉換輸入為數字
})
export {
UserRole,
baseUserSchema,
extendedUserSchema,
mergedUserSchema,
loginSchema,
publicUserSchema,
roleSchema,
advancedUserSchema,
}
現在大致上在 typescript 使用 enum 本身不會有什麼大問題,不過在比較舊的 typescript 版本中,使用 enum 會有些許問題,比如說
enum CustomEnum {
User = 'user',
Manager = 'manager',
Admin = 'admin'
}
// 舊版 typescript 在使用 Object.keys()
Object.keys(CustomEnum); // 舊版會連同 index 數量都會成為 keys 新版不會。
也因為這樣以前在處理 enum 並且要嚴謹的解決 typescript 問題時,會用另一種寫法(我們拿月份作為 enum 做顯示)
export const monthStatus = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
] as const;
export type MonthStatus = (typeof monthStatus)[number];
這時 MonthStatus
會成為 union type 所以可以完美解決相關問題,並且也可以使用 monthStatus
去解決 enum 抓值得問題。當然我沒記錯在 5.4 之後使用 enum 基本上沒有這些問題了,可以使用原生 enum 安心服用。
現在,讓我們創建一個更加強大的表單處理 composable,它充分利用了 Zod 和 TypeScript Generic的特性,以下為簡單例子:
import { computed, ref } from 'vue'
import { useForm, useField } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as zod from 'zod'
import { ZodTypeAny } from 'zod'
export function useAdvancedZodForm<T extends ZodTypeAny>(schema: T) {
type FormType = zod.infer<T>
const { handleSubmit, errors, resetForm } = useForm<FormType>({
validationSchema: toTypedSchema(schema)
})
const formData = ref<Partial<FormType>>({})
const onSubmit = (callback: (values: FormType) => void | Promise<void>) => {
return handleSubmit((values) => {
return callback(schema.parse(values))
})
}
const setFormValues = (values: Partial<FormType>) => {
Object.entries(values).forEach(([key, initialValue]) => {
const { value: fieldValue } = useField(key, undefined, { initialValue });
formData.value[key as keyof FormType] = fieldValue;
})
}
const isValid = computed(() => Object.keys(errors.value).length === 0)
return {
onSubmit,
errors,
resetForm,
setFormValues,
formData,
isValid
}
}
使用我們的高級 composable 和 Zod schema 創建一個通用表單組件:
<!-- src/components/AdvancedForm.vue -->
<template>
<form @submit="onSubmit">
<template v-for="(field, key) in fields" :key="key">
<div>
<label :for="key">{{ field.label }}</label>
<component
:is="field.component"
:id="key"
v-model="formData[key]"
v-bind="field.props"
/>
<span v-if="errors[key]">{{ errors[key] }}</span>
</div>
</template>
<button type="submit" :disabled="!isValid">Submit</button>
</form>
</template>
<script lang="ts" setup>
import { PropType } from 'vue'
import { ZodTypeAny } from 'zod'
import { useAdvancedZodForm } from '../composables/useAdvancedZodForm'
interface FieldConfig {
component: string
label: string
props?: Record<string, unknown>
}
const props = defineProps({
schema: {
type: Object as PropType<ZodTypeAny>,
required: true
},
fields: {
type: Object as PropType<Record<string, FieldConfig>>,
required: true
},
initialValues: {
type: Object as PropType<Record<string, unkown>>,
default: () => ({})
}
})
const emit = defineEmits(['submit'])
const { onSubmit, errors, resetForm, setFormValues, formData, isValid } = useAdvancedZodForm(props.schema)
setFormValues(props.initialValues)
const handleSubmit = onSubmit((data) => {
emit('submit', data)
})
</script>
最後,讓我們使用我們的高級表單組件,展示如何利用 Zod 和 TypeScript 的特性:
<!-- src/views/AdvancedUserForm.vue -->
<template>
<AdvancedForm
:schema="advancedUserSchema"
:fields="formFields"
:initial-values="initialValues"
@submit="handleSubmit"
/>
</template>
<script lang="ts" setup>
import { advancedUserSchema, UserRole } from '../schemas/advanced-user'
import AdvancedForm from '../components/AdvancedForm.vue'
const formFields = {
username: { component: 'input', label: 'Username', props: { type: 'text' } },
email: { component: 'input', label: 'Email', props: { type: 'email' } },
age: { component: 'input', label: 'Age', props: { type: 'number' } },
role: {
component: 'select',
label: 'Role',
props: {
options: Object.values(UserRole)
}
},
registrationDate: { component: 'input', label: 'Registration Date', props: { type: 'date' } },
}
const initialValues = {
username: 'johndoe',
email: 'john@example.com',
age: '30', // 注意這裡是字符串,Zod 的 coerce 會自動轉換為數字
role: UserRole.User,
registrationDate: new Date().toISOString().split('T')[0]
}
const handleSubmit = (data: z.infer<typeof advancedUserSchema>) => {
console.log('Form submitted with:', data)
// 這裡的 data 已經被 Zod 解析和轉換,所以 age 是數字,registrationDate 是 Date 對象
}
</script>
在這個深入的教程中,我們探討了如何結合 Vee-Validate 和 Zod 的進階特性,以及 TypeScript 的高級類型系統,來處理複雜的表單場景。我們特別關注了以下幾點:
extends
、merge
、pick/omit
、nativeEnum
和 enum
的差異,以及 transform
和 coerce
的使用。useAdvancedZodForm
composable,它充分利用了 Zod 和 TypeScript 的特性。通過這種方法,我們創建了一個非常靈活、類型安全且高度可維護的表單處理系統。這個系統不僅能夠處理複雜的表單驗證邏輯,還提供了出色的開發體驗和運行時性能。
在實際應用中,這種方法可以大大提高表單處理的效率和可靠性。它允許開發者快速創建複雜的表單,同時保持代碼的清晰度和類型安全性。通過充分利用 Zod 和 TypeScript 的特性,我們可以在開發階段捕獲更多潛在的錯誤,減少運行時的問題。
最後,這種方法的可擴展性也值得注意。隨著應用的增長,我們可以輕鑑添加新的 schema、擴展現有的 schema,或者創建更專門化的表單組件,而不會失去類型安全性或增加太多的複雜性。這使得它特別適合於大型或不斷發展的專案。