跳至主要内容

[typescript] TypeScript Zod

官方文件

Zod

環境設定

先初始化 TypeScript 專案:

npm create vite@latest 你的專案名稱 -- --template vanilla-ts

接著安裝 Zod:

npm install zod

並確保 tsconfig.jsonstrict 欄位為 true

tsconfig.json
{  "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

src/main.ts
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 驗證通過的話,就會直接輸出變數的值

src/main.ts
import { z } from "zod";const MessageSchema = z.string();const message = "Hello World";console.log(MessageSchema.parse(message)); // Hello World

但如果變數不符合我們定義的型別類型,VS Code 雖然不會有提示,但是瀏覽器的 console 會有錯誤出現:

src/main.ts
import { z } from "zod";const MessageSchema = z.string();const message = 123;console.log(MessageSchema.parse(message));

Image

safeParse

如果不想讓程式因為錯誤而中斷執行,則可以改用 safeParse,safeParse 會回傳一個物件,裡面就包含驗證成功或失敗的訊息:

src/main.ts
import { z } from "zod";const MessageSchema = z.string();const message = 123;console.log(MessageSchema.safeParse(message));

Image

與失敗不同,驗證通過的話,該物件內會有變數的資料:

src/main.ts
import { z } from "zod";const MessageSchema = z.string();const message = "Hello World";console.log(MessageSchema.safeParse(message));

Image

object

如果要定義一個物件型別,則可使用 z.object 搭配 z.infer

src/main.ts
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()

src/main.ts
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 就代表有錯誤發生:

src/main.ts
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);}

Image

如果只想要單純的顯示錯誤訊息,則需要安裝 Zod 的驗證套件 zod-validation-error

npm install zod-validation-error
src/main.ts
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));}

Image

default

在定義型別的同時,Zod 也允許我們定義預設值,在型別後面使用 default 即可:

src/main.ts
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);}

Image

甚至可以在 default 內使用其他 Function:

src/main.ts
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);}

Image

Validation

除了可以定義型別外,Zod 也提供許多驗證方法讓我們使用,詳細的可以參考官網,這邊簡單介紹幾個。

max & min (Number & Strings)

如果型別為 Number,則 maxmin 是用來設定最大和最小值應為多少,型別為 String 的話,則是設定字串長度最大和最小應為多少:

src/main.ts
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);}

Image

email (Strings)

判斷字串是否為 email 格式:

src/main.ts
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);}

Image

url (Strings)

判斷字串是否為 url 格式:

src/main.ts
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);}

Image

enum

使用 enum 能限制輸入的值:

src/main.ts
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);}

Image

你也可以把 enum 要放的值宣告成一個陣列,像是這樣:

src/main.ts
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 不知道我們的陣列是屬於哪種型別,要修正這個錯誤只要在陣列後面加上斷言即可:

src/main.ts
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

src/main.ts
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);}

Image

Object

Zod 物件支援的方法有些都是我們在 TypeScript 中有使用過的,例如:partialpickomit 等。

partial

使用 partial 能夠讓所有型別變為可選的:

src/main.ts
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>;

Image

這樣寫也可以:

src/main.ts
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);}

Image

pick

使用 pick 能從物件挑出要的型別:

src/main.ts
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>;

Image

omit

使用 omit 能排除不要的型別:

src/main.ts
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>;

Image

extend

使用 extend 能添加新的型別至現有的型別物件:

src/main.ts
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>;

Image

merge

如果在原先的物件型別內想要結合其他物件型別的所有欄位,則可以使用 merge

src/main.ts
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>;

Image

passthrough

如果在物件內傳入了一個額外的屬性,且該屬性沒有在型別物件內定義,預設的情況下是不會接收到這個屬性的:

src/main.ts
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);}

Image

如果堅持要把額外的屬性接收的話,可以使用 passthrough

src/main.ts
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);}

Image

strict

同上節,不想要接收額外屬性的話,可以使用 strict

src/main.ts
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);}

Image

array

如果要讓陣列內的元素都是某一型別且不為空的話,則可以這樣寫:

src/main.ts
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 能嚴謹定義陣列內的型別:

src/main.ts
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

src/main.ts
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

如果要讓一個欄位有兩種型別的話,可以這樣寫:

src/main.ts
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

src/main.ts
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

src/main.ts
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 的型別:

src/main.ts
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:

src/main.ts
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);}

參考資料

Web Dev Simplified