---
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