JWT-authentication-models-JWT 裡有提到 Zod 驗證使用者輸入的資料,這一篇會提到為什麼需要使用 Zod 來驗證。

TypeScript 需要你定義型別,強制你去思考放進去變數或函式的資料會是甚麼。但好玩的是,TypeScript 其實不會真的驗證你資料的型別。

因為 TypeScript 只在編譯層運作,而不是在運行時。如果對比一下 TypeScript 的程式碼與它轉成的 JavaScript 程式碼:

  • TypeScript 程式碼
const justAFunction = (n: number): string => {
	return `${n}`
}

console.log(justAFunction)
  • JavaScript 程式碼
"use strict";
const justAFunction = (n) => {
		return `${n}`;
};
console.log(justAFunction);

由上述可知,它只根據你程式碼的來源來檢查型別是否正確,所以它並不是真的驗證真實的資料。

檢查型別

TypeScript 只要使用得當,還是會強制你檢查不確定的型別。

將上面的範例進行修改:

const justAFunction = (str: string[] | string): string => {
  return str.join(' ')
}

console.log(justAFunction(["Hello", "World"]))
console.log(justAFunction("Hello World"))

當編譯時會產生錯誤:

index.ts:2:14 - error TS2339: Property 'join' does not exist on type 'string | string[]'.
  Property 'join' does not exist on type 'string'.

2   return str.join(' ')
               ~~~~

Found 1 error in index.ts:2

這是因為編譯器認定 strstring 型別時,它並沒有 join() 這個方式。

這個時候有兩個解決方法,第一個就是把 string 拿掉,只留下 string[],另一個就是驗證變數的的型別:

const justAFunction = (str: string[] | string): string => {
  if (typeof str === 'string') {
    return str
  }

  return str.join(' ')
}

console.log(justAFunction(["Hello", "World"]))
console.log(justAFunction("Hello World"))

它也會轉譯成 Javascript 並且被驗證型別。

對照外部資料的型別

假設一個 API 回傳一個使用者:

{
  "firstname": "John",
  "lastname": "Doe",
  "birthday": "1985-04-03"
}

我們要為這份資料建立一個 interface:

interface User {
  firstname: string
  lastname: string
  birthday: string
}

fetch 這個 API 來取回使用者資料:

const retrieveUser = async (): Promise<User> => {
  const resp = await fetch('/user/me')
  return resp.json()
}

這樣看起來沒什麼問題,但假設 birthdaytimestamp 型別,TypeScript 依舊會將這個型別視為 string,雖然有數字在裡面,但是 TypeScript 並不會檢查它真實的值。

所以老方法就是,寫一個驗證的函式:

const validate = (obj: any): obj is User => {
  return obj !== null
    && typeof obj === 'object'
    && 'firstname' in obj
    && 'lastname' in obj
    && 'birthday' in obj
    && typeof obj.firstname === 'string'
    && typeof obj.lastname === 'string'
    && typeof obj.birthday === 'string'
}

const user = await retrieveUser()

if (!validate(user)) {
  throw Error("User data is invalid")
}

這樣做確實就可以保證資料的型別,但是遇到更複雜的 API 那就會寫到死。

Zod

Zod 就是讓 TypeScript 型別強制驗證 Javascript 裡的型別。它允許你定義 schema、推斷型別、驗證資料。

讓 User 使用 Zod:

import { z } from 'zod'

const User = z.object({
  firstname: z.string(),
  lastname: z.string(),
  birthday: z.string()
})

把該 schema 提取出來成為一個類型:

const UserType = z.infer<User>

驗證的方式會像:

const userResp = await retrieveUser()
const user = User.parse(userResp)

現在就能獲得被驗證過的資料。


Reference