如何使用 TypeScript 累積類型:輸入所有可能的 fetch() 結果
當我開始(和我的團隊)用TypeScript 和Svelte(我們都討厭的JavaScript 和React)重寫我們的應用程式時,我遇到了一個問題:
如何安全地輸入 HTTP 回應的所有可能的正文?
這對你來說有啟發嗎?如果沒有,你很可能就是“其中之一”,呵呵。讓我們暫時離題一下,以便更好地理解圖片。
為什麼這個領域似乎未經探索
似乎沒有人關心 HTTP 回應的“所有可能的主體”,因為我找不到為此做的任何東西(好吧,也許是 ts-fetch)。讓我快速透過我的邏輯來解釋為什麼會這樣。
沒有人關心,因為人們:
只關心happy path:HTTP狀態碼為2xx時的反應體。
人們在其他地方手動輸入它。
對於#1,我想說的是,開發人員(尤其是沒有經驗的開發人員)忘記了 HTTP 請求可能會失敗,失敗回應中攜帶的資訊很可能與常規回應完全不同。
對於#2,讓我們深入研究 ky 和axios 等流行 NPM 套件中發現的一個大問題。
資料獲取包的問題
據我所知,人們喜歡像 ky 或 axios 這樣的包,因為它們的「功能」之一是它們會在不正常的 HTTP 狀態碼上拋出錯誤。從什麼時候開始可以這樣了?自從從來沒有。但顯然人們並沒有意識到這一點。人們很高興並且滿足於在不正常的回應中遇到錯誤。
我想人們在捕捉的時候會輸入不正常的身體。多麼混亂,多麼有代碼味道!
這是一種程式碼味道,因為您有效地使用 try..catch 區塊作為分支語句,而 try..catch 並不意味著是分支語句。
但即使你與我爭論分支在 try..catch 中自然發生,還有另一個導致這種情況仍然不好的重要原因:當拋出錯誤時,運行時需要展開調用堆疊。就 CPU 週期而言,這比使用 if 或 switch 語句的常規分支要昂貴得多。
知道了這一點,你能證明僅僅因為濫用 try..catch 區塊而造成的效能損失是合理的嗎?我說不。我想不出有什麼理由能讓前端世界對此感到非常滿意。
既然我已經解釋了我的推理,那麼讓我們回到正題吧。
問題,詳細
HTTP 回應可能會根據其狀態代碼攜帶不同的資訊。例如,接收 PATCH HTTP 請求的 todo 端點(例如 api/todos/:id)在回應狀態碼為 200 時可能會傳回具有不同正文的回應,而回應狀態碼為 400 時可能會傳回不同正文的回應。
舉例:
// For the 200 response, a copy of the updated object: { "id": 123, "text": "The updated text" } // For the 400 response, a list of validation errors: { "errors": [ "The updated text exceeds the maximum allowed number of characters." ] }
因此,考慮到這一點,我們回到問題陳述:如何鍵入執行此 PATCH 請求的函數,其中 TypeScript 可以告訴我正在處理哪個主體,具體取決於我編寫的 HTTP 狀態碼程式碼?答:使用串流語法(建構器語法、鍊式語法)來累積類型。
建構解決方案
讓我們先定義一個基於先前類型建構的類型:
export type AccumType<T, NewT> = T | NewT;
超級簡單:給定類型 T 和 NewT,將它們連接起來形成一個新類型。在 AccumType 中再次使用這個新類型作為 T,然後就可以累積另一個新類型。然而,這手工完成的並不好。讓我們介紹一下解決方案的另一個關鍵部分:流暢的語法。
流暢的語法
給定類別 X 的一個對象,其方法始終返回其自身(或自身的副本),可以將方法呼叫一個接一個地連結起來。這是流暢的語法,或是鍊式語法。
讓我們寫一個簡單的類別來執行此操作:
export class NamePending<T> { accumulate<NewT>() { return this as NamePending<AccumType<T, NewT>>; } } // Now you can use it like this: const x = new NamePending<{ a: number; }>(); // x is of type NamePending<{ a: number; }>. const y = x.accumulate<{ b: string; }> // y is of type NamePending<{ a: number; } | { b: string; }>.
尤里卡!我們已經成功地將流暢的語法和我們編寫的類型結合起來,開始將資料類型累積為單一類型!
如果不明顯,您可以繼續練習,直到累積了所需的類型(x.accumulate().accumulate()…直到完成)。
這一切都很好,但是這個超級簡單的類型並沒有將 HTTP 狀態碼綁定到相應的正文類型。
完善我們所擁有的
我們想要的是為 TypeScript 提供足夠的信息,以便其類型縮小功能發揮作用。為此,我們需要執行必要的操作來取得與原始問題相關的程式碼(在每個檔案中鍵入 HTTP 回應的正文) -狀態碼基礎)。
首先,重新命名並進化 AccumType。下面的程式碼顯示了迭代的進展:
// Iteration 1. export type FetchResult<T, NewT> = T | NewT; // Iteration 2. export type FetchResponse<TStatus extends number, TBody> = { ok: boolean; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends number, NewT> = T | FetchResponse<TStatus, NewT>; //Makes sense to rename NewT to TBody.
此時,我意識到:狀態代碼是有限的:我可以(並且確實)查找它們並為它們定義類型,並使用這些類型來限制類型參數 TStatus:
// Iteration 3. export type OkStatusCode = 200 | 201 | 202 | ...; export type ClientErrorStatusCode = 400 | 401 | 403 | ...; export type ServerErrorStatusCode = 500 | 501 | 502 | ...; export type StatusCode = OkStatusCode | ClientErrorStatusCode | ServerErrorStatusCode; export type NonOkStatusCode = Exclude<StatusCode, OkStatusCode>; export type FetchResponse<TStatus extends StatusCode, TBody> = { ok: TStatus extends OkStatusCode ? true : false; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends StatusCode, TBody> = T | FetchResponse<TStatus, TBody>;
我們已經得到了一系列非常漂亮的類型:透過基於 ok 或 status 屬性上的條件進行分支(編寫 if 語句),TypeScript 的類型縮小功能將啟動!不信,我們寫一下class部分來試試:
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
試駕一下:
// For the 200 response, a copy of the updated object: { "id": 123, "text": "The updated text" } // For the 400 response, a list of validation errors: { "errors": [ "The updated text exceeds the maximum allowed number of characters." ] }
現在應該清楚為什麼類型縮小能夠根據 status 屬性的 ok 屬性正確預測分支時主體的形狀。
但是,有一個問題:實例化類別時的初始類型,在上面的註解區塊中標記。我是這樣解決的:
export type AccumType<T, NewT> = T | NewT;
這個小改變有效地排除了最初的輸入,我們現在開始營業了!
現在我們可以寫如下程式碼,Intellisense 將 100% 準確:
export class NamePending<T> { accumulate<NewT>() { return this as NamePending<AccumType<T, NewT>>; } } // Now you can use it like this: const x = new NamePending<{ a: number; }>(); // x is of type NamePending<{ a: number; }>. const y = x.accumulate<{ b: string; }> // y is of type NamePending<{ a: number; } | { b: string; }>.
在查詢 ok 屬性時,類型縮小也會起作用。
如果您沒有註意到,我們可以透過不拋出錯誤來編寫更好的程式碼。根據我的專業經驗,axios 是錯的,ky 是錯的,其他 fetch helper 做同樣的事情都是錯的。
結論
TypeScript 確實很有趣。透過結合 TypeScript 和 Fluent 語法,我們能夠根據需要累積盡可能多的類型,這樣我們就可以從第一天開始編寫更準確、更清晰的程式碼,而不是一遍又一遍地調試。這項技術已被證明是成功的,並且可供任何人嘗試。安裝 dr-fetch 並測試它:
// Iteration 1. export type FetchResult<T, NewT> = T | NewT; // Iteration 2. export type FetchResponse<TStatus extends number, TBody> = { ok: boolean; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends number, NewT> = T | FetchResponse<TStatus, NewT>; //Makes sense to rename NewT to TBody.
更複雜的包
我還創建了 wj-config,一個旨在完全消除過時的 .env 檔案和 dotenv 的軟體包。該套件還使用此處教授的 TypeScript 技巧,但它使用 & 而不是 | 連接類型。如果您想嘗試一下,請安裝 v3.0.0-beta.1。不過,打字要複雜得多。在 wj-config 之後進行 dr-fetch 簡直就是在公園散步。
有趣的東西:那裡有什麼
讓我們看看與 fetch 相關的套件中的一些錯誤。
同構獲取
您可以在自述文件中看到:
// Iteration 3. export type OkStatusCode = 200 | 201 | 202 | ...; export type ClientErrorStatusCode = 400 | 401 | 403 | ...; export type ServerErrorStatusCode = 500 | 501 | 502 | ...; export type StatusCode = OkStatusCode | ClientErrorStatusCode | ServerErrorStatusCode; export type NonOkStatusCode = Exclude<StatusCode, OkStatusCode>; export type FetchResponse<TStatus extends StatusCode, TBody> = { ok: TStatus extends OkStatusCode ? true : false; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends StatusCode, TBody> = T | FetchResponse<TStatus, TBody>;
「伺服器回應錯誤」? ?沒有。 「伺服器說你的請求不好」。是的,投擲部分本身就很糟糕。
ts 獲取
這個想法是正確的,但不幸的是只能輸入 OK 與非 OK 回應(最多 2 種類型)。
凱
我最批評的軟體包之一,展示了這個例子:
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
這是一個非常初級的開發人員會寫的:只是快樂的道路。根據其自述文件,等效性:
const x = new DrFetch<{}>(); // Ok, having to write an empty type is inconvenient. const y = x .for<200, { a: string; }>() .for<400, { errors: string[]; }>() ; /* y's type: DrFetch<{ ok: true; status: 200; statusText: string; body: { a: string; }; } | { ok: false; status: 400; statusText: string; body: { errors: string[]; }; } | {} // <-------- WHAT IS THIS!!!??? > */
投擲部分太糟糕了:為什麼要分支到投擲,強迫你稍後接住?這對我來說毫無意義。錯誤中的文字也具有誤導性:它不是「獲取錯誤」。抓取成功了。你得到了回應,不是嗎?你只是不喜歡它……因為這不是快樂的道路。更好的措詞是「HTTP 請求失敗:」。失敗的是請求本身,而不是取得操作。
以上是如何使用 TypeScript 累積類型:輸入所有可能的 fetch() 結果的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

不同JavaScript引擎在解析和執行JavaScript代碼時,效果會有所不同,因為每個引擎的實現原理和優化策略各有差異。 1.詞法分析:將源碼轉換為詞法單元。 2.語法分析:生成抽象語法樹。 3.優化和編譯:通過JIT編譯器生成機器碼。 4.執行:運行機器碼。 V8引擎通過即時編譯和隱藏類優化,SpiderMonkey使用類型推斷系統,導致在相同代碼上的性能表現不同。

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

從C/C 轉向JavaScript需要適應動態類型、垃圾回收和異步編程等特點。 1)C/C 是靜態類型語言,需手動管理內存,而JavaScript是動態類型,垃圾回收自動處理。 2)C/C 需編譯成機器碼,JavaScript則為解釋型語言。 3)JavaScript引入閉包、原型鍊和Promise等概念,增強了靈活性和異步編程能力。

JavaScript不需要安裝,因為它已內置於現代瀏覽器中。你只需文本編輯器和瀏覽器即可開始使用。 1)在瀏覽器環境中,通過標籤嵌入HTML文件中運行。 2)在Node.js環境中,下載並安裝Node.js後,通過命令行運行JavaScript文件。
