在上篇文章中,我已稍微提及了本次專案所使用的驗證工具——Zod。Zod 是專為 TypeScript 設計的資料驗證工具,就像人體的免疫系統,確保資料結構在 runtime 時與 TypeScript 的靜態型別一致,彌補了 TypeScript 僅在編譯時檢查靜態型別的不足。Zod 主打的特色是可以藉由設置驗證的 Schema ,並通過 TypeScript 的型別推斷功能,自動產生相對應的型別,這避免了開發者重複手動定義型別的需要。除此之外,Zod 還具備一套完整的錯誤訊息系統,大大減少了開發者需要自行定義驗證邏輯和錯誤處理的複雜性。
parse
或 safeParse
驗證和確保外來資料的正確型別。如果熟悉 TypeScript 的使用邏輯,其實在使用 Zod 時會覺得格外親切,例如我想要定義標單中的驗證及型別
先定義 scheme 再轉換為 type。
特別注意因為 TypeScript 的型別規則,所以 Zod 預設就是 required 屬性
const formValuesSchema = z.object({
customImage: z.string(),
title: z.string(),
description: z.string(),
themeColor: z.enum(['basic', 'blue-rose', 'lime'])
})
type FormValues = z.infer<typeof formValuesSchema>
上方範例中我的 FormValues 就會是:
type FormValues = {
title: string;
customImage: string;
description: string;
themeColor: "basic" | "blue-rose" | "lime";
}
設定好了規則,我們還需要設定錯誤訊息,
required_error
invalid_type_error
const loginSchema = z.object({
email: z.string({
required_error: 'Email 為必填欄位',
invalid_type_error: '必須為字串'
}),
password: z.string({ required_error: 'Password 為必填欄位' })
})
想要依據不同狀態設置不同訊息,Zod 也提供 errorMap
屬性可以使用
issue code 可以參考:https://github.com/colinhacks/zod/blob/master/ERROR_HANDLING.md#error-handling-in-zod
const loginSchema = z.object({
email: z.string({
required_error: 'Email 為必填欄位',
invalid_type_error: '必須為字串',
description: '請輸入正確的 Email',
errorMap: (issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
if (issue.received === 'undefined') {
return { message: 'Email 為必填欄位' }
}
if (issue.received === 'number') {
return { message: '不可為數字' }
}
}
return { message: ctx.defaultError }
}
}),
password: z.string({ required_error: 'Password 為必填欄位' })
})
假如我希望 string、number 等型別可以有像是長度或是特殊規格,Zod 也有提供許多常用方法:
const signUpSchema = z.object({
username: z
.string({ required_error: 'Username 為必填欄位' })
.regex(/^[a-zA-Z0-9_]*$/),
email: z
.string({ required_error: 'Email 為必填欄位' }).email(),
password: z
.string({ required_error: 'Password 為必填欄位' }).min(8)
})
另外看到很特別的有 url()
、emoji()
、ip()
、includes(string)
number 類型則有 gt(number)
、gte(number)
、lt(number)
、gt(number)
、lte(number)
、positive()
、negative()
等方法
一樣將 message 寫在參數中即可
const signUpSchema = z.object({
username: z
.string({ required_error: 'Username 為必填欄位' })
.regex(
/^[a-zA-Z0-9_]*$/,
'只能包含英文、數字及底線,不可包含空白及特殊符號'
),
email: z
.string({ required_error: 'Email 為必填欄位' })
.email('請輸入正確的 Email'),
password: z
.string({ required_error: 'Password 為必填欄位' })
.min(8, '密碼長度不可小於 8 個字元')
})
Zod 提供了兩種 null 的方法
nullable()
:數值可以為 nullnullish()
:數值可以為 null 或 undefinedconst schema = z.object({
customImage: z.string().url().nullable(),
title: z.string().nullable(),
description: z.string().max(300, '字數不可超過 300 字').nullable(),
themeColor: z.enum(['basic', 'blue-rose', 'lime'])
})
我的範例中有使用到 enum 的型別,Zod 除了提供自己的方法也可以使用原生的 enum 設定
使用 nativeEnum
定義 schema
enum ThemeColor {
BASIC = 'basic',
BLUE_ROSE = 'blue-rose',
LIME = 'lime'
}
const themeColor = z.nativeEnum(ThemeColor)
const themeColor = z.enum(['basic', 'blue-rose', 'lime'])
// 或是
const COLORS = ['basic', 'blue-rose', 'lime'] as const
const themeColor = z.enum(COLORS)
Zod 提供一個方法 - catch()
可以將錯誤的值替換為希望的值
範例說明:如果
themeColor
不為'basic', 'blue-rose', 'lime'
,設為‘basic’
const schema = z.object({
customImage: z.string().url().nullable(),
title: z.string().nullable(),
description: z.string().max(300, '字數不可超過 300 字').nullable(),
themeColor: z.enum(['basic', 'blue-rose', 'lime']).catch('basic')
})
如同 typeScript 的 Record 泛型,Zod 也可以使用這個方法。第一個參數為 key 的型別,第二參數為 value 型別
const genericFieldsSchema = z.record(z.string(), z.string().nullable())
可以使用 union()
、merge()
或 extends()
union()
建立一個聯合型別(union type)
const signUpSchema = z.object({
username: z
.string({ required_error: 'Username 為必填欄位' })
.regex(/^[a-zA-Z0-9_]*$/),
email: z
.string({ required_error: 'Email 為必填欄位' }).email(),
password: z
.string({ required_error: 'Password 為必填欄位' }).min(8)
})
const genericFieldsSchema = z.record(z.string(), z.string().nullable())
const unionSchema = z.union([signUpSchema, genericFieldsSchema])
extend()
可以從一個現有的 schema 繼承,並添加或覆蓋 field。
const baseSchema = z.object({
username: z
.string({ required_error: 'Username 為必填欄位' })
.regex(/^[a-zA-Z0-9_]*$/),
password: z
.string({ required_error: 'Password 為必填欄位' }).min(8)
});
const extendedSchema = baseSchema.extend({
email: z
.string({ required_error: 'Email 為必填欄位' }).email(),
});
merge()
合併兩個 schema,但不支持 field 的覆蓋
const baseSchema = z.object({
username: z
.string({ required_error: 'Username 為必填欄位' })
.regex(/^[a-zA-Z0-9_]*$/),
password: z
.string({ required_error: 'Password 為必填欄位' }).min(8)
});
const emailSchema = z.object({
email: z.string({ required_error: 'Email 為必填欄位' }).email(),
});
const mergedSchema = baseSchema.merge(emailSchema);
Zod 當然也提供了除了正則以外的自定義規則方法 - refine()
或 superRefine()
refine
:回傳一個布林值來表示驗證是否成功,第一個參數為自定義驗證的 callback,第二個參數則是錯誤訊息。superRefine
:callback 提供兩個參數:
⚠️ 特別注意如果回傳錯誤請使用
ctx.addIssue()
先判斷錯誤類型再輸入錯誤訊息進行錯誤處理,而不是直接回傳布林值!
const schema = z
.object({
id: z.string().nullable(),
title: z.string().nullable(),
url: z.string().url().nullable(),
type: z
.record(z.string(), z.string())
.catch({ id: 'default', label: '請選擇' }),
order: z.number().int().nullable()
})
.superRefine((val, ctx) => {
if (val.type.id === 'website') {
if (!val.title)
return ctx.addIssue({ // 使用 ctx.addIssue 做錯誤處理
code: z.ZodIssueCode.custom, // 使用自定義的錯誤類型
message: '請輸入連結名稱' // 錯誤訊息
})
}
})
通常從 API 回傳的資料型別會是 unknown ,所以我們可以藉由 Zod 提供的 parse 和 safeParse 進行檢驗
ZodError
。這表示需要在使用 parse
時使用 try/catch
來處理錯誤。success
布林值,表示驗證是否成功。
success
為 true
,回傳的物件也包含一個 data
屬性,該屬性包含驗證過的資料。success
為 false
,回傳的物件也包含一個 error
屬性,是一個 ZodError
實例,顯示驗證失敗的原因。// currentUser 是由後端回傳的資料
const themeColorSchema = z.enum(['basic', 'blue-rose', 'lime'])
const currentUserSchema = z.object({
username: z.string(),
customImage: z.string().nullable(),
image: z.string().nullable(),
title: z.string().nullable(),
description: z.string().nullable(),
themeColor: themeColorSchema
})
type CurrentUser = z.infer<typeof currentUserSchema>
const result = currentUserSchema.safeParse(user).success // true or false
const isValidCurrentUser = (user: unknown): user is CurrentUser => {
return currentUserSchema.safeParse(user).success
}
if (!isValidCurrentUser(currentUser)) {
return <div>Something went wrong</div>
}
起初想要使用是因為可以進行表單驗證,但越深入學習越發現他的功能讓開發省了很多程序,也慢慢了解這個工具越來越火紅的道理,這篇文章羅列了一些在專案中使用到的功能,也是在這次開發中新學習到的工具,還有很多其他的功能沒敘述到,也可以在日後慢慢研究。最後想要分享一個關於結合 react-form-hook 的型別問題:
❓在自定義 Input 元件中,我將 react-form-hook 的 register 作為參數傳進來,型別定義為 register: UseFormRegister<FieldValues>
,UseFormRegister
和 FieldValues
皆由 react-form-hook 提供,其中 FieldValues
的型別為
interface FieldValues {
[x: string]: any
}
以下是 useForm 的 value 定義的 schema
const schema = z.object({
customImage: z.string().url().nullable(),
title: z.string().nullable(),
description: z.string().max(300, '字數不可超過 300 字').nullable(),
themeColor: z.enum(['basic', 'blue-rose', 'lime']).catch('basic')
})
type FormValues = z.infer<typeof schema>
這時候問題就出現了:type FieldValues = z.infer<typeof schema>
與 Input 元件中的 FieldValues
型別不符合
所以解決的方法是將 FormValues 也加上 FieldValues 的型別:
定義為 genericFieldsSchema
const schema = z.object({
customImage: z.string().url().nullable(),
title: z.string().nullable(),
description: z.string().max(300, '字數不可超過 300 字').nullable(),
themeColor: z.enum(['basic', 'blue-rose', 'lime']).catch('basic')
})
const genericFieldsSchema = z.record(z.string(), z.string().nullable())
const unionSchema = z.union([schema, genericFieldsSchema])
type FormValues = z.infer<typeof unionSchema>
這是目前想到的解決方法,分享給如果有遇到相同情況的讀者們,如果有更好的方法也歡迎提供!!那麼今日份的進度就到驗證結束了~
https://ithelp.ithome.com.tw/articles/10305282