[typescript] TypeScript Zod
官方文件
環境設定
先初始化 TypeScript 專案:
npm create vite@latest 你的專案名稱 -- --template vanilla-ts
接著安裝 Zod:
npm install zod
並確保 tsconfig.json
的 strict
欄位為 true
:
{ "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "lib": ["ESNext", "DOM"], "moduleResolution": "Node", "strict": true, "resolveJsonModule": true, "isolatedModules": false, "esModuleInterop": true, "noEmit": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "skipLibCheck": true }, "include": ["src"]}
Basic
Primitives
import { z } from "zod";// primitive valuesz.string();z.number();z.bigint();z.boolean();z.date();z.symbol();// empty typesz.undefined();z.null();z.void(); // accepts undefined// catch-all types// allows any valuez.any();z.unknown();// never type// allows no valuesz.never();
parse
要使用 Zod 進行型別驗證的話,必須先定義型別,在使用 parse
來驗證你的變數是否有包含該型別。
要是 parse 驗證通過的話,就會直接輸出變數的值
import { z } from "zod";const MessageSchema = z.string();const message = "Hello World";console.log(MessageSchema.parse(message)); // Hello World
但如果變數不符合我們定義的型別類型,VS Code 雖然不會有提示,但是瀏覽器的 console 會有錯誤出現:
import { z } from "zod";const MessageSchema = z.string();const message = 123;console.log(MessageSchema.parse(message));
safeParse
如果不想讓程式因為錯誤而中斷執行,則可以改用 safeParse
,safeParse 會回傳一個物件,裡面就包含驗證成功或失敗的訊息:
import { z } from "zod";const MessageSchema = z.string();const message = 123;console.log(MessageSchema.safeParse(message));
與失敗不同,驗證通過的話,該物件內會有變數的資料:
import { z } from "zod";const MessageSchema = z.string();const message = "Hello World";console.log(MessageSchema.safeParse(message));
object
如果要定義一個物件型別,則可使用 z.object
搭配 z.infer
:
import { z } from "zod";const UserSchema = z.object({ username: z.string(), age: z.number(),});/** * type User = { * username: string; * age: number; * } */type User = z.infer<typeof UserSchema>;const Wei: User = { username: "Wei", age: 25,};console.log(Wei);
optional
在 TypeScript 內,如果要讓型別是可選的話,則會在型別名稱加上 ?
,而如果是使用 Zod 則需要在型別後面加上 .optional()
:
import { z } from "zod";const UserSchema = z.object({ username: z.string(), age: z.number().optional(),});type User = z.infer<typeof UserSchema>;const Wei: User = { username: "Wei", age: 25,};console.log(Wei);
Error Handling
如果需要詳細查看型別錯誤給予的錯誤訊息,則可以先判斷 safeParse().success
回傳的結果是否回 false,false 就代表有錯誤發生:
import { z } from "zod";const UserSchema = z.object({ username: z.string(), age: z.number().optional(),});type User = z.infer<typeof UserSchema>;const Wei: User = { username: 1, age: -1,};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);}
如果只想要單純的顯示錯誤訊息,則需要安裝 Zod 的驗證套件 zod-validation-error
:
npm install zod-validation-error
import { z } from "zod";import { fromZodError } from "zod-validation-error";const UserSchema = z.object({ username: z.string({ invalid_type_error: "username 欄位應為字串型別" }), age: z.number().optional(),});type User = z.infer<typeof UserSchema>;const Wei: User = { username: 1, age: -1,};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(fromZodError(result.error));}
default
在定義型別的同時,Zod 也允許我們定義預設值,在型別後面使用 default
即可:
import { z } from "zod";const UserSchema = z.object({ username: z.string().default("user"), age: z.number().optional(),});type User = z.infer<typeof UserSchema>;const Wei = { age: 25,};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
甚至可以在 default
內使用其他 Function:
import { z } from "zod";const UserSchema = z.object({ username: z.string().default("user-" + crypto.randomUUID()), age: z.number().optional(),});type User = z.infer<typeof UserSchema>;const Wei = { age: 25,};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
Validation
除了可以定義型別外,Zod 也提供許多驗證方法讓我們使用,詳細的可以參考官網,這邊簡單介紹幾個。
max & min (Number & Strings)
如果型別為 Number
,則 max
和 min
是用來設定最大和最小值應為多少,型別為 String
的話,則是設定字串長度最大和最小應為多少:
import { z } from "zod";const UserSchema = z.object({ username: z.string().max(2,{message : "username 欄位的字串最大長度應為 2"}), age: z.number().min(1,{message : "age 欄位的最小值應為 1"}).optional(),});});type User = z.infer<typeof UserSchema>;const Wei: User = { username: "Wei", //字串最大長度應為 2 age: -1, //數字最小值應為 0};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
email (Strings)
判斷字串是否為 email
格式:
import { z } from "zod";const UserSchema = z.object({ username: z.string(), age: z.number().optional(), email: z.string().email().optional(),});type User = z.infer<typeof UserSchema>;const Wei: User = { username: "Wei", age: 25, email: "yher25gmail.com",};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
url (Strings)
判斷字串是否為 url
格式:
import { z } from "zod";const UserSchema = z.object({ username: z.string(), age: z.number().optional(), website: z.string().url().optional(),});type User = z.infer<typeof UserSchema>;const Wei: User = { username: "Wei", age: 25, website: "httpsweiyun0912.github.io/Wei-Docusaurus/",};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
enum
使用 enum
能限制輸入的值:
import { z } from "zod";const UserSchema = z.object({ username: z.string(), age: z.number().optional(), hobby: z.enum(["Sleep", "Eat", "Drink"]), // hobby 只能接收 "Sleep", "Eat", "Drink"});type User = z.infer<typeof UserSchema>;const Wei = { username: "Wei", age: 25, hobby: "",};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
你也可以把 enum
要放的值宣告成一個陣列,像是這樣:
import { z } from "zod";const hobbies = ["Sleep", "Eat", "Drink"];const UserSchema = z.object({ username: z.string(), age: z.number().optional(), hobby: z.enum(hobbies),});type User = z.infer<typeof UserSchema>;const Wei = { username: "Wei", age: 25, hobby: "Eat",};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
雖然執行起來沒問題,但編譯器會出現錯誤,這是因為我們的 hobbies
陣列是能改變的, Zod 不知道我們的陣列是屬於哪種型別,要修正這個錯誤只要在陣列後面加上斷言即可:
import { z } from "zod";const hobbies = ["Sleep", "Eat", "Drink"] as const;// 這樣也可以// const hobbies = <const>["Sleep","Eat","Drink"] ;const UserSchema = z.object({ username: z.string(), age: z.number().optional(), hobby: z.enum(hobbies),});type User = z.infer<typeof UserSchema>;const Wei = { username: "Wei", age: 25, hobby: "Eat",};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
nativeEum
如果想要用 TypeScript
來另外定義 enum
的話,則可以使用 nativeEum
:
import { z } from "zod";enum Hobbies { Sleep = "sleep", Eat = "eat", Drink = "drink",}const UserSchema = z.object({ username: z.string(), age: z.number().optional(), hobby: z.nativeEnum(Hobbies),});type User = z.infer<typeof UserSchema>;const Wei = { username: "Wei", age: 25, hobby: Hobbies.Eat,};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
Object
Zod 物件支援的方法有些都是我們在 TypeScript 中有使用過的,例如:partial
、pick
、omit
等。
partial
使用 partial
能夠讓所有型別變為可選的:
import { z } from "zod";const hobbies = <const>["Sleep", "Eat", "Drink"];const UserSchema = z .object({ username: z.string(), age: z.number(), hobby: z.enum(hobbies), }) .partial();type User = z.infer<typeof UserSchema>;
這樣寫也可以:
import { z } from "zod";const hobbies = <const>["Sleep", "Eat", "Drink"];const UserSchema = z.object({ username: z.string(), age: z.number(), hobby: z.enum(hobbies),});type User = z.infer<typeof UserSchema>;const Wei = { username: "Wei",};const result = UserSchema.partial().safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
pick
使用 pick
能從物件挑出要的型別:
import { z } from "zod";const hobbies = <const>["Sleep", "Eat", "Drink"];const UserSchema = z .object({ username: z.string(), age: z.number(), hobby: z.enum(hobbies), }) .pick({ username: true });type User = z.infer<typeof UserSchema>;
omit
使用 omit
能排除不要的型別:
import { z } from "zod";const hobbies = <const>["Sleep", "Eat", "Drink"];const UserSchema = z .object({ username: z.string(), age: z.number(), hobby: z.enum(hobbies), }) .omit({ username: true });type User = z.infer<typeof UserSchema>;
extend
使用 extend
能添加新的型別至現有的型別物件:
import { z } from "zod";const hobbies = <const>["Sleep", "Eat", "Drink"];const UserSchema = z .object({ username: z.string(), age: z.number(), hobby: z.enum(hobbies), }) .extend({ email: z.string() });type User = z.infer<typeof UserSchema>;
merge
如果在原先的物件型別內想要結合其他物件型別的所有欄位,則可以使用 merge
:
import { z } from "zod";const hobbies = <const>["Sleep", "Eat", "Drink"];const FoodSchema = z.object({ name: z.string(), price: z.number(), amount: z.number(),});const UserSchema = z .object({ username: z.string(), age: z.number(), hobby: z.enum(hobbies), }) .merge(FoodSchema);type User = z.infer<typeof UserSchema>;
passthrough
如果在物件內傳入了一個額外的屬性,且該屬性沒有在型別物件內定義,預設的情況下是不會接收到這個屬性的:
import { z } from "zod";const UserSchema = z.object({ username: z.string(),});type User = z.infer<typeof UserSchema>;const Wei = { username: "Wei", age: 20,};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
如果堅持要把額外的屬性接收的話,可以使用 passthrough
:
import { z } from "zod";const UserSchema = z .object({ username: z.string(), }) .passthrough();type User = z.infer<typeof UserSchema>;const Wei = { username: "Wei", age: 20,};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
strict
同上節,不想要接收額外屬性的話,可以使用 strict
:
import { z } from "zod";const UserSchema = z .object({ username: z.string(), }) .strict();type User = z.infer<typeof UserSchema>;const Wei = { username: "Wei", age: 20,};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
array
如果要讓陣列內的元素都是某一型別且不為空的話,則可以這樣寫:
import { z } from "zod";const UserSchema = z.object({ username: z.string(), friends: z.array(z.string()).nonempty(),});type User = z.infer<typeof UserSchema>;const Wei = { username: "Wei", friends: ["1", "2", "3"],};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
tuple
使用 Tuple 能嚴謹定義陣列內的型別:
import { z } from "zod";const UserSchema = z.object({ username: z.string(), friends: z.tuple([z.string(), z.string()]), //只能裝兩個字串元素});type User = z.infer<typeof UserSchema>;const Wei = { username: "Wei", friends: ["1", "2"],};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
如果想要讓 Tuple 更加彈性的話,可以使用 rest
:
import { z } from "zod";const UserSchema = z.object({ username: z.string(), // 第一個元素和第二個元素為字串型別,剩下的元素僅能是數字型別 friends: z.tuple([z.string(), z.string()]).rest(z.number()),});type User = z.infer<typeof UserSchema>;const Wei = { username: "Wei", friends: ["1", "2", "3"], // 發生錯誤,第三個元素應為 數字型別};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
union
如果要讓一個欄位有兩種型別的話,可以這樣寫:
import { z } from "zod";const UserSchema = z.object({ id: z.string().or(z.number()), // id 可以是 字串型別 或 數字型別 username: z.string(),});type User = z.infer<typeof UserSchema>;const Wei = { id: "1", username: "Wei",};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
或是用 union
:
import { z } from "zod";const UserSchema = z.object({ id: z.union([z.string(), z.number()]), // id 可以是 字串型別 或 數字型別 username: z.string(),});type User = z.infer<typeof UserSchema>;const Wei = { id: "1", username: "Wei",};const result = UserSchema.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
record
如果不想定義物件的 key 名稱,但又想指定物件內的值都一定要是某種型別,則可以使用 record
:
import { z } from "zod";const UserMap = z.record(z.string()); // 物件內的值(value) 僅能是 字串型別const Wei = { id: "1", username: "Wei", aaa: "test",};const result = UserMap.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
我們也可以定義 key 的型別:
import { z } from "zod";const UserMap = z.record(z.string(), z.string()); // {string : string}const Wei = { id: "1kk", username: "Wei",};const result = UserMap.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}
當然你也可以結合先前學到的 validation:
import { z } from "zod";// key 的字串長度最少為 1, value 的字串長度最多為 30const UserMap = z.record(z.string().min(1), z.string().max(30));const Wei = { id: "1kk", username: "Wei",};const result = UserMap.safeParse(Wei);if (!result.success) { console.log(result.error.issues);} else { console.log(result);}