TypeScript + Zod 入門:前端型別驗證的實務基礎

TypeScript

在現代前端開發中,TypeScript 已成為提升程式可維護性與穩定度的重要工具。TypeScript的一大優勢就是可以在開發期間避免因為型別錯誤而發生難以查找的bug,以下是T編在專案中使用TypeScript總結的一些重點,並進一步介紹 Zod 作為 Runtime 驗證工具,透過實際範例示範 TypeScript 與 Zod 在 API 邊界的整合方式,說明如何在資料進入 UI 前完成驗證,建立更可靠的前端型別驗證流程。

一、什麼是TypeScript?

根據官方網站的說明:

TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.

意思就是TypeScript是一個以JavaScript為基底的程式語言,讓你在開發時有較好的開發體驗。

舉一個例子,先宣告一個user的物件並設置一個name的參數,但如果想印出age時JavaScript會直接印出undefined,同時TypeScript在編譯期就會直接報錯:

error TS2339: Property 'age' does not exist on type '{ name: string; }'.

const user = {
	name:"Tony"
}
console.log(user.age);

在編譯期就幫你檢查是否有型別錯誤可以降低 debug 成本、減少低級錯誤,不會有不小心手誤打錯卻找不到的問題。

二、定義後端api的型別

當前端開發時,通常會與後端有密切的合作,我們可以先定義好後端api傳回來的response的樣子,比如:有一支API叫/api/users 好了,回傳的樣子可能是

[
  { "name": "Tony", "age": 23 },
  { "name": "Kevin", "age": 24 },
  { "name": "Jim", "age": 25 },
  { "name": "Leo", "age": 26 }
]

我們就可以先定義一個user型別

type User = {
	name:string;
	age:number;
}

假如你已經寫好了一個useUsersData的hook,可能是

export const useUsersData= () => {
  const { data, isPending, error } = useQuery<User[]>({ 
  //指定回傳的型別為一個User的陣列
  //這裡的 <User[]> 只是提供開發期型別提示,並不會在 Runtime 驗證回傳資料。
    queryKey: ["userData"],
    queryFn: async ({ signal }) => {
      const { json } = await httpClient("/api/users", {
        method: "GET",
        signal,
      });
      return json;
    },
    staleTime: 5 * 60 * 1000,
  });

  return {
    users: data,
    loading: isPending,
    error,
  };
};

在component裡就可以直接拿users?.[0]?.name等等,一樣如果你試圖拿users?.[0]?.age TypeScript就會自動幫你檢查到錯誤,並出現紅字提醒你。不過要注意:TypeScript 的型別檢查只發生在編譯期。當程式真的跑起來後(Runtime),如果後端回來的資料格式不符合預期(例如回傳錯誤物件或欄位缺漏),TypeScript 本身是擋不住的,這時就需要 Zod 這類 Runtime validator。

三、Zod(API邊界驗證)

甚麼是Zod?我們一樣來看官方文件的介紹:

Zod is a TypeScript-first validation library. Using Zod, you can define schemas you can use to validate data, from a simple string to a complex nested object.

翻譯成中文就是Zod是一個TypeScript優先的驗證套件,可以透過宣告schemas 來做到資料的驗證,從簡單的字串到複雜的巢狀物件它都可以掌握。

那一樣我們可以先宣告一個Schema

import { z } from "zod";
 
// 定義驗證規則
const UserSchema = z.object({ 
  name: z.string(),
  age: z.number()
});

// 定義 User 陣列的驗證規則 (這很重要,因為 API 回傳的是陣列)
const UsersSchema = z.array(UserSchema);

用TypeScript加Zod最大的優勢是他們可以很好的整合在一起,我們剛剛定義的User Type就可以改寫成:

// User 就會自動繼承你在 Schema 定義的型別,不用手寫兩次
type User = z.infer<typeof UserSchema>;

接著我們要去進行驗證,我們要把資料parse出來

UserSchema.parse({ name: "Tony", age: 23 }); 

可以利用try/catch進行錯誤處理

try {
  UserSchema.parse({ name: 42, age: "100" });
} catch(error){
  if(error instanceof z.ZodError){
    console.log(error.issues);
    /* [
      {
        expected: 'string',
        code: 'invalid_type',
        path: [ 'name' ],
        message: 'Invalid input: expected string'
      },
      {
        expected: 'number',
        code: 'invalid_type',
        path: [ 'age' ],
        message: 'Invalid input: expected number'
      }
    ] */
  }
}
//如果不想讓他報錯可以使用safeParse
const result = UserSchema.safeParse({ name: 42, age: "100" });
//safeParse 不會丟 exception,而是回傳 { success, data/error },方便在程式流程中分支處理

實務應用: 通常在 fetcher 或 queryFn 拿到 JSON 的那一刻就做 parse,確保進入 UI Component 的資料都是 100% 可信的。把剛剛的程式碼拿來修改就變成:

export const useUsersData= () => {
  const { data, isPending, error } = useQuery<User[]>({
    queryKey: ["userData"],
    queryFn: async ({ signal }) => {
      const { json } = await httpClient("/api/users", {
        method: "GET",
        signal,
      });
      // 在這裡直接做 parse,如果格式不對會直接噴錯,保護 UI
      // 注意:因為 API 回傳的是陣列,所以要用 z.array(UserSchema) 或定義好的 UsersSchema
      return UsersSchema.parse(json);
    },
    staleTime: 5 * 60 * 1000,
  });

  return {
    users: data,
    loading: isPending,
    error,
  };
};

這樣一來就可以在不只編譯期抓錯,也可以在實際的Runtime下也可以發現錯誤,讓開發更加的順利。

四、同場加映:自動生成 API 型別

如果後端有提供 Swagger/OpenAPI 文件,便可以利用 openapi-typescript-codegen 來自動生成型別,不用一個一個手敲。

安裝: npm install --save-dev openapi-typescript-codegen

接著執行: openapi --input ./spec.json (你的文件路徑或網址) --output ./generated (你希望生成檔案的位置)

你的 generated 資料夾就會自動跑出一堆已經定義好的型別文件了。


參考資料:

Loading

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *