/
CATEGORY
JavaScript
/
什麼是 ts-rest?論 TypeScript Interface 的局限性與前後端導入 ts-rest 的優缺點

什麼是 ts-rest?論 TypeScript Interface 的局限性與前後端導入 ts-rest 的優缺點

MUKI AI Summary

在前後端開發中,使用 TypeScript Interface 來定義 API 型別存在一些局限性,如前後端型別不一致和型別驗證不嚴謹等問題。這些問題在專案變得複雜時尤其明顯,因為 API 回應資料來自外部服務,無法保證每次都符合定義的型別。為了解決這些問題,ts-rest 提供了一種自動化的型別同步工具,能夠在前後端之間自動同步 API 型別,並結合 Zod 進行動態驗證,確保資料完整性。

然而,ts-rest 的最佳使用方式是前後端共同導入,這在台灣的產業生態中並不容易實現。即便如此,前端單獨導入 ts-rest 仍能透過 Zod 提供的型別驗證機制,減少錯誤發生的機率,相較於手動建立 TypeScript Interface,ts-rest 能夠提供更強大的型別檢查與資料驗證,特別適合大型或 API 變動頻繁的專案。儘管如此,對於小型或型別驗證需求不高的專案,手動建立 TypeScript Interface 仍然是一個合理的選擇...

在前後端開發的日常裡,呼叫 API 幾乎是無法避免的任務。我通常會用 axios 來發請求,再用 try-catch 處理錯誤。但每次面對不同的 API 回應資料時,手動檢查和處理型別總覺得有些麻煩,尤其是在專案越來越複雜的情況下。你可能也遇過,明明 API 文件說得清清楚楚,但回傳的資料結構卻不如預期,或者回應少了一些關鍵欄位,這種情況通常會導致一些意想不到的錯誤。

使用 TypeScript Interface 碰到的問題

以往我是使用 TypeScript Interface,幫助我們在開發時檢查型別,但我發現光靠 TypeScript 並不足以完全解決這些問題,因為 API 回應資料來自外部服務,不能保證每次回來的資料都符合我們定義的型別。這時候就得自己補上大量的型別檢查邏輯,寫得好累。

先說說我之前使用 TypeScript interface 來定義 API 的狀況。我會定義一個 interface 來描述 API 的請求和回應資料結構,像是這樣:

interface Post {
  id: number;
  title: string;
  content: string;
}

當我發送 API 請求時,我會使用這個 interface 來確保請求回來的資料符合預期。表面上看起來都很美好,TypeScript 可以確保我在開發時得到型別提示。

但隨著專案越來越大,我開始碰到幾個問題:

  1. 前後端型別不一致:當後端對 API 進行調整,改變回應的資料結構時。如果我沒有即時更新前端的 interface,可能會在測試或生產環境才發現型別有誤。
  2. 型別驗證不嚴謹:雖然 TypeScript interface 能幫助我在開發階段發現一些問題,但如果後端返回了不符合型別的資料,TypeScript 沒辦法在執行時檢查這個錯誤。換句話說,僅靠 interface,資料到前端後依然可能出現不一致。

這些問題會讓整個 API 通訊過程變得脆弱,尤其是當專案變大,或有多個開發團隊時,我得花很多時間檢查並同步前後端的型別定義。後來,我開始思考,有沒有一個更好的方式,能讓前後端的型別同步變得自動化,並且在執行時保證資料的完整性?

直到我發現了 ts-rest 這個酷東西

ts-rest 如何解決這些問題

ts-rest 出現後,改變了我處理前後端型別一致性的方式。它讓我能夠使用一套工具,在前後端之間自動同步 API 的型別,這樣一來,我不需要再手動定義和維護前端的 API 型別了。

我用一個簡單的範例說明 ts-rest 怎麼解決 TypeScript interface 的痛點。

前後端的型別同步

使用 ts-rest 後,後端的型別定義可以自動生成前端的 API 型別,這樣我就不用擔心因為忘記更新 interface 而導致 API 錯誤了。

▼ 後端使用 contract 來定義 api 路由,這邊還有用到 Zod (z.{number, string}) 來協助驗證資料

const c = initContract();
export const postsContract = c.router({
  getPosts: {
    method: "GET",
    path: "/posts",
    responses: {
      200: z.array(z.object({
        id: z.number(),
        title: z.string(),
        content: z.string(),
      })),
    },
  },
});

透過 ts-rest,這個 API 定義不僅可以在後端使用,前端也能自動拿到型別,確保 API 請求的資料結構一致。而且,在 API 回應時,Zod 會自動驗證資料是否符合定義的結構。

執行時檢查型別

傳統的 TypeScript interface 是靜態檢查,無法在 API 請求的執行階段發現錯誤。而我用 ts-rest 搭配 Zod 後,可以在 API 回應的資料上進行動態驗證,確保即使後端發生變更,或返回不符合預期的資料,我都能在執行時立即捕捉到錯誤。

這樣的好處是,當 API 請求發生異常時,我能夠快速定位並解決問題,而不是等到出錯後再花時間去找原因。

前後端的合作

看下來,也許你會發現一件事情,就是要使用 ts-rest,最好的方式是前後端都要導入 ts-rest 機制,透過後端產生 API 定義讓前端使用。

那麼,問題來了,以台灣的產業生態來看,前後端往往被分成兩個獨立的職業,前端負責使用者介面和互動,後端則負責資料處理和伺服器邏輯。這樣的分工使得「前後端同時導入 ts-rest」變得不太容易。畢竟,後端工程師已經習慣了自己的一套開發流程,要他們改用新的工具或是架構,不僅需要額外的學習成本,還得說服他們這樣的變動能夠帶來足夠的好處。而對於一個前端工程師來說,影響後端技術決策的權限通常有限,可能我們提出了導入 ts-rest 的建議,但後端未必會接受,畢竟對他們來說,可能會覺得這是前端工程師的「需求」。

因此,從我的角度來看,前端想要強迫後端導入 ts-rest 並不現實。這就導致了一個實際的問題:如果後端不使用 ts-rest,前端單獨導入 ts-rest 是否仍然有效?或者,僅僅依賴手動編寫 TypeScript 的 interface,是否會是更好的選擇?

這種情況下,ts-rest 的部分好處,像是更嚴格的型別同步,就無法完全發揮了,但至少在前端,我仍然可以利用 ts-rest 提供的功能來建立自己的 API 合約,透過 Zod 進行型別驗證,這樣即使後端不使用 ts-rest,我依然可以確保從 API 回應中獲得的資料是符合預期的。相對來說,手動建立 TypeScript interface 雖然也能做到基本的型別檢查,但缺乏了像 ts-rest 那樣的自動化優勢和嚴謹的檢查機制,也讓我需要自己負責更多的檢查和維護工作。

我也整理了一張表,比較前端使用 ts-rest,以及手動建立 Typescript Interface 的差別,供大家參考

前端導入 ts-rest vs. 手動建立 TypeScript Interface

比較項目前端導入 ts-rest手動建立 TypeScript interface
型別同步- 需要手動撰寫 API 的定義,無法自動與後端同步
- 因為後端沒有使用 ts-rest,必須自行確認 API 文件與後端保持一致
- 完全手動撰寫和維護型別
- 容易出現前後端型別不一致的情況,特別是當 API 變更時
開發體驗- 使用 ts-rest 提供的 API 工具可以減少手寫請求邏輯,並與 TypeScript 內建的型別系統結合
- 可以定義 API 的各種操作方法
- 必須手動處理所有 API 請求邏輯
- TypeScript 的型別僅提供靜態檢查,無法處理動態資料的驗證
資料驗證 (runtime)- 結合 Zod,可以對 API 回應進行型別檢查,保證資料符合預期
- 額外的 Zod 驗證可以補充 TypeScript 無法檢查動態資料的限制
- 無內建的資料驗證功能,必須手動撰寫驗證邏輯
- TypeScript 僅在編譯階段檢查,無法在執行階段進行驗證,遇到型別錯誤時需手動排查
代碼維護性- 一旦定義好 API,ts-rest 可以保持較高的一致性
- 若後端 API 改變,依然需要手動修改 API 型別定義
- 當後端 API 發生變更時,需要手動更新 API 型別
學習曲線- 需要額外學習 ts-rest 的語法與整合方式
- 需了解 Zod 的驗證工具來搭配使用
- 熟悉 TypeScript interface 就能直接上手
- 無需額外學習框架或工具,但需要撰寫更多自訂邏輯
適用場景- 適合需要更強型別驗證和自動化工具的專案
- 有 API 變動頻繁,且需要保證資料安全性的場景
- 適合小型專案或 API 相對穩定的場景
- 對於型別驗證需求不高的情況,手動建立 interface 也足夠
維護成本與程式碼量- 必須撰寫 ts-rest API 定義與 Zod 驗證規則,初期設定會增加程式碼的負擔
- 減少後期維護成本,長期來看程式碼負擔不會過大
- 每個 API 需要單獨定義 interface 和處理驗證邏輯
- 雖然初期程式碼負擔小,但隨著 API 增加,維護成本會上升

小結

  • 若專案需求:
    • 需要強大的型別檢查與資料驗證,且 API 請求邏輯複雜,可以考慮使用 ts-rest,即使後端沒有使用,前端也能享受 Zod 結合 TypeScript 的驗證優勢。
    • API 變更頻繁,或項目規模較大時,ts-rest 能夠減少你手動維護型別和驗證的工作量。
  • 若專案需求:
    • 小型專案或 API 較穩定,且型別驗證需求不高,則可以繼續使用手動建立 TypeScript interface,因為這樣更簡單,也不需要額外學習新工具。

總而言之,即使後端不使用 ts-rest,我在前端仍然可以受益於它的型別驗證機制,尤其是配合 Zod 使用時,能夠顯著減少錯誤的發生。但若後端也能導入 ts-rest,這會使整個 API 管理流程更加高效,型別也能夠自動同步,進而達到全端一致的型別保障。不過使用 Typescript interface 依然是一些簡單或小型專案的合理選擇。

結論

我一開始在玩 ts-rest 時,也是只從前端導入 ts-rest,但光這樣,就發現了一些有趣且實用的功能。

接下來,我想透過一系列的文章,深入介紹 ts-rest 的各種應用,從如何與 Zod 結合進行型別驗證,到如何在前後端協作中發揮它的最大效益。希望這些分享能讓你對 ts-rest 有更全面的了解,並在你的開發過程中找到適合的使用場景。

歡迎給我點鼓勵,讓我知道你來過 :)

7
MUKI says:

如果文章有幫助到你,歡迎分享給更多人知道。文章內容皆為 MUKI 本人的原創文章,我會經常更新文章以及修正錯誤內容,因此轉載時建議保留原出處,避免出現版本不一致或已過時的資訊。

本文地址:https://muki.tw/ts-rest-introduce/ 已複製

Subscribe
Notify of
guest

0 則留言
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Copyright © since 2008 MUKI space* / omegaSS theme All Rights Reserved.