ホームページ >ウェブフロントエンド >jsチュートリアル >TypeScript を使用して型を蓄積する方法: 可能なすべての fetch() 結果を入力する
TypeScript と Svelte (私たち皆が嫌う JavaScript と React でした) でアプリケーションを (チームと一緒に) 書き直し始めたとき、次の問題に直面しました。
HTTP 応答の可能なすべての本文を安全に入力するにはどうすればよいですか?
これを聞いてピンときますか?そうでないなら、あなたもおそらく「そのうちの一人」でしょう、ふふ。この図をよりよく理解するために、少し脱線しましょう。
HTTP 応答の「考えられるすべての本体」については誰も気にしていないようです。これは、このために既に作成されたもの (まあ、おそらく ts-fetch) が見つからなかったためです。なぜそうなるのか、私の論理をここで簡単に説明しましょう。
人々は次のいずれかの理由で誰も気にしません:
ハッピー パスのみに注意してください: HTTP ステータス コードが 2xx の場合の応答本文。
他の場所に手動で入力します。
#1 については、そうです、開発者 (特に経験の浅い開発者) は、HTTP リクエストが失敗する可能性があること、失敗した応答に含まれる情報が通常の応答とはまったく異なる可能性が高いことを忘れていると思います。
#2 では、ky や axios などの人気のある NPM パッケージで見つかった大きな問題を掘り下げてみましょう。
私が知る限り、人々は ky や axios のようなパッケージを好みます。その「機能」の 1 つは、OK 以外の HTTP ステータス コードでエラーをスローすることです。いつからOKになったんですか?以来。しかし、どうやら人々はこれを理解していないようです。人々は満足しており、OK 以外の応答でエラーが発生することに満足しています。
人々はキャッチするときに、OK ではないボディを入力すると思います。何という混乱、何というコードの匂い!
これはコードの臭いです。これは、try..catch ブロックを分岐ステートメントとして効果的に使用しているためであり、try..catch は分岐ステートメントであることを意図したものではありません。
しかし、分岐は try..catch で自然に発生するという私に反論があったとしても、これが依然として問題である大きな理由はもう 1 つあります。エラーがスローされると、ランタイムは呼び出しスタックを巻き戻す必要があるからです。これは、if または switch ステートメントによる通常の分岐よりも、CPU サイクルの点ではるかにコストがかかります。
これを知っていると、try..catch ブロックを誤用したためだけにパフォーマンスが低下することを正当化できますか?私はノーと言います。フロントエンドの世界がこれに完全に満足しているように見える理由は 1 つも思いつきません。
私の推論の流れを説明したので、本題に戻りましょう。
HTTP 応答には、ステータス コードに応じて異なる情報が含まれる場合があります。たとえば、PATCH HTTP リクエストを受信する api/todos/:id などの todo エンドポイントは、レスポンスのステータス コードが 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 として使用すると、別の新しい型を蓄積できます。ただし、これを手作業で行うのは好ましくありません。ソリューションのもう 1 つの重要な要素である流暢な構文を紹介しましょう。
メソッドが常に自分自身 (または自分自身のコピー) を返すクラス 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; }>.
エウレカ!流暢な構文と、データ型を 1 つの型に蓄積し始めるために作成した型をうまく組み合わせることができました!
それが明らかでない場合は、目的のタイプが蓄積されるまで演習を続けることができます (x.accumulate().accumulate()…完了するまで)。
これはすべて良いことであり、素晴らしいことですが、この非常に単純なタイプでは、HTTP ステータス コードが対応するボディ タイプに関連付けられていません。
私たちが望んでいるのは、TypeScript の型絞り込み機能が作動するのに十分な情報を TypeScript に提供することです。これを行うには、元の問題に関連するコードを取得する必要があります (HTTP 応答の本文を per 形式で入力します)。 -ステータスコードベース)。
まず、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 の型絞り込み機能が作動します。信じられない場合は、クラス部分を作成して試してみましょう:
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 も間違っており、同じことをしている他のフェッチ ヘルパーも間違っています。
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.
また、廃止された .env ファイルと dotenv を完全に削除することを目的としたパッケージ、wj-config も作成しました。このパッケージも、ここで説明した TypeScript トリックを使用しますが、型を | ではなく & で結合します。試してみたい場合は、v3.0.0-beta.1 をインストールしてください。ただし、型指定ははるかに複雑です。 wj-config の後に dr-fetch を作成するのは、公園を散歩するようなものでした。
フェッチ関連のパッケージに存在するエラーをいくつか見てみましょう。
README で次のことがわかります:
// 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 応答と非 OK 応答 (最大 2 種類) しか入力できません。
私が最も批判したパッケージの 1 つは、次の例を示しています。
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
これは、非常に若い開発者が書くものです: まさに幸せな道。 README によれば、同等性は次のとおりです。
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 中国語 Web サイトの他の関連記事を参照してください。