Typescript: 他實際上沒有驗證你的型別

Typescript: 他實際上沒有驗證你的型別

更新於 發佈於 閱讀時間約 9 分鐘

Typescript: It's not actually validating your types. - DEV Community 👩‍💻👨‍💻

raw-image

Typescript 是個好東西: 他讓你定義型別, 確保你的類別和函式保持特定的預期. 他強制讓你去思考你放進函式的資料是甚麼, 你從他得到甚麼. 如果理解錯誤, 試著要呼叫一個預期應該要是 string 的函式, 卻帶著 number 給他, 編譯器就會讓你知道. 所以說他是個好東西.

有時他會讓你誤解: 我遇到一個相信 typescript 保證型別就是你說的那樣. 但我必須告訴你: Typescript 不是這樣做的.

為何? Typescript 是在編譯層運作, 而不是在 runtime. 如果你看一下 Typescript 產生的程式碼長相, 你就會看到他轉換成 Javascript 然後抽離所有型別.

Typescript 程式碼:

const justAFunction = (n: number): string => {
return `${n}`
}

console.log(justAFunction)

Javascript 程式碼的結果(假設你是轉譯成比較近期的 EcmaScript 版本):

"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

編譯器強制認定 str 的變數型別. 其中一個解法是只允許 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, 然後型別就被驗證了. 這個情況下, 我們只會保證他是一個 string 而且我們只假設他是個陣列.

在大多數情況中, 這樣已足夠. 但是一旦我們需要處理額外的資料來源 — 像是 APIs, JSON 檔案, 使用者輸入 和 類似的東西 — 我們就無法認定資料的正確. 所以我們應該要驗證資料, 因而有機會確保正確的型態.

讓外部資料對照到你的型別

所以第一步要解決這個問題大概會是建立真實反應你資料的型別.

假設 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()
}

這樣做有效, 而且 typescript 會辨識使用者的型別. 但是他可能會騙你. 假設生日包含的是 timestamp 數字(可能對於 1970 以前出生的人會某些問題… 但不是現在的重點). 這個型別仍然會將生日視為一個 string, 雖然確實有數字在裡面… 然後 Javascript 會將他視為數字. 因為, 就像我們所說的一樣, 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")
}

這樣做, 我們可以確保資料是如同他所宣稱的那樣. 但是你可能發現, 這樣做針對更複雜的案例很快就會失控.

有些 protocols 天生處理型別: gRPC, tRPC, 根據 schemaGraphQL 驗證 JSON(到特定程度上). 那些通常是針對特定使用情境. 我們可能需要更 general 的方式.

進入 Zod

Zod 是 Typescript 的型別之間失去的那個連結, 他強制驗證 Javascript 的型別. 他允許你定義結構, 推斷型別與驗證資料.

User 型別會被定義像是這樣:

import { z } from 'zod'

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

然後型別可以從結構被抽出(被推斷).

const UserType = z.infer<User>

然後驗證會長得像這樣

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

現在我們有型別和驗證過的資料, 而且我們需要寫的程式碼只比沒有驗證函式的程式碼稍微多一點.

結論

當運用 Typescript, 重要的是知道編譯器檢查和 runtime 驗證的差異. 要確保外部資料符合我們的型別, 我們需要在某處有做驗證. Zod 是很棒的工具, 正好是成本低且彈性的處理方式.

avatar-img
Chaol Liu的沙龍
0會員
5內容數
留言
avatar-img
留言分享你的想法!
Chaol Liu的沙龍 的其他內容
TransformStream is now supported cross-browser (web.dev) 現在串流轉換在 Chrome, Safari 和 Firefox 都已支援, 他們終於準備好迎接黃金時代!
當你買一輛車或腳踏車, 你第一件事會先調整座位的位置和手把的高度到適合你的身體大小. 這就跟 git 配置是一樣的. 在這篇文章, 我會分享一些 git 的設定, 而且是我一直都在使用的.
今天我們引入了 Build Output API, 一個檔案系統為基底的規格, 允許任何框架都可以讓 Vercel 建置, 並且取用了 Vercel 的優勢, 也就是 Vercel 的 infrastructure building blocks, 像是 Edge Functions, Edge M
TransformStream is now supported cross-browser (web.dev) 現在串流轉換在 Chrome, Safari 和 Firefox 都已支援, 他們終於準備好迎接黃金時代!
當你買一輛車或腳踏車, 你第一件事會先調整座位的位置和手把的高度到適合你的身體大小. 這就跟 git 配置是一樣的. 在這篇文章, 我會分享一些 git 的設定, 而且是我一直都在使用的.
今天我們引入了 Build Output API, 一個檔案系統為基底的規格, 允許任何框架都可以讓 Vercel 建置, 並且取用了 Vercel 的優勢, 也就是 Vercel 的 infrastructure building blocks, 像是 Edge Functions, Edge M