--- tags: typescript --- # TypeScript WTF - Api response with Type ![](https://i.imgur.com/X99BrSS.png) > 本文章的所有範例都可以在[這裡](https://github.com/oaoxd0314/typescript_sandbox.git)找到 TypeScript 基本上可以在 client side 進行各種型別推斷與驗證,但是卻有一個地方是不在 client side 掌控範圍的,那就是 Api Response。 ## TypeScript 的漏洞? API 基本上可以說是 client side 所有資料的來源,但這麼重要的地方,我們卻不能像 TS 一樣在編譯之前就找到錯誤,實在是超級不合理。 因此今天就來了解一下,該用怎麼樣的方式對來自 server 的 API response 做驗證,確保他不會成為整份 project 的漏洞。 而 API 和前端所溝通的資料,基本上 87% 都是 JSON,假設我們透過 API 拿到了這樣的一組產品資料: > 資料來源:https://dummyjson.com/docs/products ```json= { "products": [ { "id": 1, "title": "iPhone 9", "description": "An apple mobile which is nothing like apple", "price": 549, "discountPercentage": 12.96, "rating": 4.69, "stock": 94, "brand": "Apple", "category": "smartphones", "thumbnail": "...", "images": ["...", "...", "..."] }, {...}, {...}, {...} // 30 items ], "total": 100, "skip": 0, "limit": 30 } ``` 那問題來了: - 我們該如何驗證這筆資料呢,怎樣才算是正確的呢? - 各個欄位的意義是什麼呢 `skip`? `id`? `rating`? `stock`? - `brand` ,`category` 有明文型別 (Literal Type) 來確定哪些才是有的嗎 - `discountPercentage` 可以為 0 嗎 --- 而 Json 在 parse 之前只能算 `string`,這讓驗證更是難上加難 就連 TypeScript 官方被開了這樣一個 [issue](https://github.com/microsoft/TypeScript/issues/1897),至今爲止都還沒被 close 掉。 > 這個問題大致上就是在說,`fetch` 回來的 `json` 格式,會被 TypeScript 推斷為 `Unknow`,因此要請 TS 官方生出一個 Json 的原生型別,但定義太廣了,直到今天都還沒辦法解決 > 有興趣的話同樣也可以看看這個[github討論串](https://github.com/node-fetch/node-fetch/issues/1262),都是在說同樣一件事。 只知道會拿到怎麼樣的資料問題還是一大堆,有沒有一個辦法,只需要一份檔案,就能讓工程師和程式碼都能夠看懂呢? ## Json Schema 根據[官方](https://json-schema.org/)介紹,使用 Json Schema 能夠提幫助達成: 1. 描述資料格式(Describes your existing data format(s)). 2. 乾淨,且人類和機器都能讀懂(Provides clear human- and machine- readable documentation). 3. 能夠幫助驗證 (Validates data which is useful for): - 自動測試的資料(Automated testing)aka Mock Data - 前端回傳給 Server 的資料(Ensuring quality of client submitted data) 定義一份 Json Schema 最基本的欄位,你需要填寫以下幾個: - `$schema`: 根據哪份 schema 協定撰寫。 - `$id`: 這份 schema 的放置位址 - `title` `description`: 純粹拿來描述這份 schema - `type`: 這份 schema 定義的資料型別(最外層)是什麼,如果是一份 `json` 通常就是 `object`,或是你也有可能直接拿到陣列,那就填寫 `array` 所以最基礎的部分會像這樣子: ```json= { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/product.schema.json", "title": "Product", "description": "A product in the catalog", "type": "object" } ``` 接著再去定義 json 會拿到的資料,例如: ```json= "properties": { "limit": { "description": "product 的最大回傳筆數", "type": "number" } }, ``` 或是你需要限制這個屬性不能為 0 或是負數: ```json= "properties": { "limit": { // ... "exclusiveMinimum": 0 } }, ``` > 其他相關設定例如:哪些是必要、巢狀結構(如 production )該如何定義,就去他的[官方教學](https://json-schema.org/learn/getting-started-step-by-step)看吧,因為今天不是講 Json Schema 的,再說下去太歹戲拖棚 Json schema 的好處有目共睹,但奴性不堅強如你,看到一半一定會想,關我屁事,為什麼我要維護這份文檔啊??? ## Zod ![](https://i.imgur.com/uxtGtmJ.png) 身為一個工程師,能夠坐著就不站著,能夠自動就不手動,`Zod` 就是一個可以幫助我們自動產生 Json schema 的好棒棒工具。 根據[作者的說明](https://github.com/colinhacks/zod#introduction),Zod 是一個 TypeScript-first 的驗證、定義 schema 的函式庫,這裡的 schema 泛指 `string` 到 `object` 的所有資料型別。 除了會依據你給定的格式創造 schema 以外,`Zod` 還能幫推斷出靜態的 typescript type, 更具有以下這些優點: - 0 套件依賴,不會遇到依賴混亂(Dependency Confusion) - 所有瀏覽器,包含 node 都可以進行編譯 - 超輕,只有 8kb - 生成出來的 type 不會被外部影響 - 具有連結性的 interface(介面) - 解析,不驗證( parse, don't validate ),這部分的好處要看完[文章](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)會比較好懂 - 不綁定 TS,意思是連 JS 都可有一個像樣的型別系統可用 使用方法就如同上面提到的 `parse, don't validate`: ```typescript= import { z } from "zod"; // 簡單建立一個 schema const mySchema = z.string(); // parsing mySchema.parse("tuna"); // => "tuna" mySchema.parse(12); // => 型別不對 throws ZodError // "safe" parsing 就是不會丟 error 的 parse,會在結果跟你說有沒有出錯 mySchema.safeParse("tuna"); // => { success: true; data: "tuna" } mySchema.safeParse(12); // => { success: false; error: ZodError } ``` 因此我們就可以用 Zod 來幫助我們建立 schema,就拿一開始的[例子](https://dummyjson.com/docs/products)來說: > 所有範例都可以在[這裡](https://github.com/oaoxd0314/typescript_sandbox.git)找到 ```typescript= const productResponse = z.object({ total: z.number(), skip: z.number(), limit: z.number(), products: z.array(productsSchema) }) ``` 就可以將已知的 schema 定義成這樣,接著,來嘗試 parse API response 看看吧! ```typescript= const data = await fetch('https://dummyjson.com/products') .then(response=>response.json()) const parseData = type.productResponse.parse(data) ``` 將滑鼠移到 parseData 就可以看到 zod 實際上幫你 parse 出的 type 長怎樣了 ![](https://i.imgur.com/N68oaG6.png) 如此一來就連 JSON response 都可以驗證了! 當然,就如同 TypeScript 一樣,他可以做的事遠不止如此,更詳細的教學只要看看[官方文檔](https://github.com/colinhacks/zod#basic-usage) 就能夠輕易學會啦。 ### 主委加碼: 直接定義 response 如果你簡單粗暴,不管什麼 json schema / 引用別人的專案,想通通自己來,可以做到嗎? 答案是,可以,那就會回到一開始提到的問題,`response.json()` 的推斷型別是 `Unknow`。 那要怎麼處理這個問題呢,首先我們需要知道 `Unknow` 是個什麼東西。 在[官方文檔](https://www.typescriptlang.org/docs/handbook/type-compatibility.html#any-unknown-object-void-undefined-null-and-never-assignability) 有這張關於那些定義 "沒有" "不知道" "不清楚" 的型別大亂鬥 ![](https://i.imgur.com/uB0WFv8.png) 但其實 `Unknow` 可以是**任何型別**,看上圖就知道,它包含的東西和 `any` 不相上下 兩者間最根本的差別在於,`Unknow` 是**需要去另外推斷**的,你不能因為 TS 把它定義為 `Unknow` 就完事,還需要去做例如 `as Array` 之類的事情把詳細的型別定義出來(類似 Todo?) 基於以上,我們就可以對被推斷為 `unknow` 的 json 做一些小小的改變,例如透過泛形把定義這件事挪到真正需要 call 到 api 才實現: ```typescript= async function request<T>(url: string): Promise<T> { return await fetch(url).then((response) => response.json() as any as T) } ``` > 原始程式參考自 [^註1] 接著就可以靠我們自己定義的 `interface` 或是 `type` 對這個 json 物件作定義啦: ```typescript= type ProductRespType ={ total: number, skip: number, limit: number, products: Record<string,unknow>[] } const response =request<ProductRespType>('https://dummyjson.com/products') ``` # 結語 `Zod` 或許不是很完美,但至少有了一個起頭,可以不用撰寫煩人的 Json Schema 就能對 json(aka 其實是 string 吧) 進行驗證。 也不需要擔心 schema 在前端的使用場景被限制,還是可以透過 `z.infer` 將 schema 轉為 type: ```typescript= const productResponse = z.object({ total: z.number(), skip: z.number(), limit: z.number(), products: z.array(productSchema) }) type test = z.infer< typeof productResponse> ``` 而且這是一個相當大的開源社群,有不少人正在嘗試以 zod 為基礎創造工具出來,或許[其中就有一個](https://github.com/colinhacks/zod#Ecosystem)就是你真正需要的。 ## reference - https://dummyjson.com/docs/products - https://medium.com/geekculture/mocking-in-typescript-e02e028d0537 - https://ithelp.ithome.com.tw/articles/10281033 - https://github.com/google/intermock - https://blog.ah.technology/auto-generate-typescript-d-ts-from-your-api-requests-responses-4d44b9f2d138 - https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ - https://www.typescriptlang.org/docs/handbook/type-compatibility.html#any-unknown-object-void-undefined-null-and-never-assignability - 以及我自己 [^註1]:https://www.newline.co/@bespoyasov/how-to-use-fetch-with-typescript--a81ac257