首頁 >web前端 >js教程 >如何使用 TypeScript 累積類型:輸入所有可能的 fetch() 結果

如何使用 TypeScript 累積類型:輸入所有可能的 fetch() 結果

Barbara Streisand
Barbara Streisand原創
2024-12-06 12:40:15880瀏覽

How to Use TypeScript to Accumulate Types: Typing ALL possible fetch() Results

當我開始(和我的團隊)用TypeScript 和Svelte(我們都討厭的JavaScript 和React)重寫我們的應用程式時,我遇到了一個問題:

如何安全地輸入 HTTP 回應的所有可能的正文?

這對你來說有啟發嗎?如果沒有,你很可能就是“其中之一”,呵呵。讓我們暫時離題一下,以便更好地理解圖片。

為什麼這個領域似乎未經探索

似乎沒有人關心 HTTP 回應的“所有可能的主體”,因為我找不到為此做的任何東西(好吧,也許是 ts-fetch)。讓我快速透過我的邏輯來解釋為什麼會這樣。

沒有人關心,因為人們:

  1. 只關心happy path:HTTP狀態碼為2xx時的反應體。

  2. 人們在其他地方手動輸入它。

對於#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中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn