実際の例を使用した高度なTypeScript(パート1)

公開: 2020-11-06

先週、TypeScriptについて少し紹介しました。具体的には、JavaScriptを拡張するこの言語が、より堅牢なコードの作成にどのように役立つかについて話しました。 それは単なる紹介だったので、プロジェクトで使用したい(そしておそらく必要な)TypeScript機能のいくつかについては話しませんでした。

今日は、TypeScriptを実際のプロジェクトに専門的に適用する方法をお教えします。 これを行うには、まずNelio Contentのソースコードの一部を調べて、どこから始め、現在どのような制限があるかを理解します。 次に、完全に型指定されたコードができるまで、少しずつ改善を加えて、元のJavaScriptコードを徐々に改善します。

NelioContentのソースコードをベースとして使用

ご存知かもしれませんが、Nelio Contentは、ソーシャルメディアでWebサイトのコンテンツを共有できるプラグインです。 これに加えて、投稿の品質分析、作成する必要のある今後のコンテンツを追跡するための編集カレンダーなど、ブログでより良いコンテンツを常に生成できるようにすることを目的としたいくつかの機能も含まれています。 。

NelioContentの編集カレンダー
NelioContentの編集カレンダー。

先月、プラグインの視覚的および内部的な完全な再設計であるバージョン2.0を公開しました。 このバージョンは、ReactインターフェイスやReduxストアなど、今日WordPressで利用できるすべての新しいテクノロジー(最近ブログで話しました)を使用して作成しました。

したがって、今日の例では、後者を改善します。 つまり、Reduxストアを入力する方法を見ていきます。

Nelioコンテンツ編集カレンダーセレクター

編集カレンダーは、曜日ごとにスケジュールしたブログ投稿を表示するユーザーインターフェイスです。 つまり、少なくとも、Reduxストアには2つのクエリ操作が必要です。1つは特定の日にスケジュールされた投稿を通知し、もう1つは投稿IDを指定してすべての属性を返します。

このテーマに関する投稿を読んだとすると、Reduxのセレクターが最初のパラメーターとして、アプリのすべての情報とそれに続く必要な追加パラメーターを含む状態を受け取ることをすでに知っています。 したがって、JavaScriptの2つのセレクターの例は次のようになります。

 function getPost( state, id ) { return state.posts[ id ]; } function getPostsInDay( state, day ) { return state.days[ day ] ?? []; }

州にposts属性とdays属性があることをどうやって知っているのか疑問に思っているなら、それは非常に簡単です。私がそれらを定義したからです。 しかし、これが私がこのようにそれらを実装することに決めた理由です。

1日の投稿と、IDによる投稿という、2つの異なる観点から情報にアクセスできるようにしたいと考えています。 したがって、データを2つの部分に編成することは理にかなっているようです。

  • 一方では、サーバーから取得してReduxストアに保存したすべての投稿を一覧表示するposts属性があります。 論理的には、それらを配列に保存し、順次検索して、IDが予想される投稿と一致する投稿を見つけることもできますが、オブジェクトは辞書のように動作し、より高速な検索を提供します。
  • 一方、特定の日にスケジュールされている投稿にもアクセスする必要があります。 繰り返しになりますが、単一の配列を使用してすべての投稿を保存し、それをフィルタリングして特定の日に属する投稿を見つけることもできますが、さらに別の辞書を使用すると、より高速な検索ソリューションが提供されます。

Nelioコンテンツのアクションとレデューサー

最後に、動的なカレンダーが必要な場合は、ストアに保存されている情報を更新できる関数を実装する必要があります。 簡単にするために、2つの簡単な方法を提案します。1つはカレンダーに新しい投稿を追加できる方法で、もう1つは既存の投稿の属性を変更できる方法です。

Reduxストアのアップデートには2つの部分が必要です。 一方では、実行したい変更を通知するアクションがあり、他方では、ストアの現在の状態と更新を要求するアクションが与えられると、現在の状態に必要な変更を適用するレデューサーがあります。新しい状態を生成します。

したがって、これを考慮に入れると、これらは私たちの店で行う可能性のあるアクションです。

 function receiveNewPost( post ) { return { type: 'RECEIVE_NEW_POST', post, }; } function updatePost( postId, attributes ) { return { type: 'UPDATE_POST', postId, attributes, } }

そしてここにレデューサーがあります:

 function reducer( state, action ) { state = state ?? { posts: {}, days: {} }; const postIds = Object.keys( state.posts ); switch ( action.type ) { case 'RECEIVE_NEW_POST'; if ( postIds.includes( action.postId ) ) { return state; } return { posts: { ...state.posts, [ action.post.id ]: action.post, }, days: { ...state.days, [ action.post.day ]: [ ...state.days[ action.post.day ], action.post.id, ], }, }; case 'UPDATE_POST'; if ( ! postIds.includes( action.postId ) ) { return state; } const post = { ...state.posts[ action.postId ], ...action.attributes, }; return { posts: { ...state.posts, [ post.id ]: post, }, days: { ...Object.keys( state.days ).reduce( ( acc, day ) => ( { ...acc, [ day ]: state.days[ day ].filter( ( postId ) => postId !== post.id ), } ), {} ), [ post.day ]: [ ...state.days[ post.day ], post.id, ], }, }; } return state; }

時間をかけてすべてを理解し、前進しましょう!

JavaScriptからTypeScriptへ

最初にすべきことは、前のコードをTypeScriptに変換することです。 TypeScriptはJavaScriptのスーパーセットであるため、すでにそうなっています…しかし、前の関数をコピーしてTypeScript Playgroundに貼り付けると、暗黙の型がanyである変数が多すぎるため、コンパイラがかなり文句を言うことがわかります。 それでは、最初にいくつかの基本的なタイプを明示的に追加して、これを修正しましょう。

私たちがしなければならないのは、「複雑」なもの(アプリケーションの状態など)にany型を明示的に追加し、 numberstring 、またはその他の変数/引数に必要なものを使用することだけです。 たとえば、元のJavaScriptセレクターは次のとおりです。

 function getPost( state, id ) { return state.posts[ id ]; }

明示的なTypeScriptタイプを使用すると、次のようになります。

 function getPost( state: any, id: number ): any | undefined { return state.posts[ id ]; }

ご覧のとおり、コードを入力するという単純なアクション(「ジェネリック型」を使用している場合でも)は、一目で多くの情報を提供します。 基本的なJavaScriptと比較して明らかに改善されています! この場合、たとえば、 getPostnumberを期待し(投稿IDは整数です、覚えていますか?)、結果は投稿が存在する場合は何か( any )、存在しない場合は何もありません( undefined )。

ここに、コンパイラが文句を言わないように、単純型を使用したすべてのコード型とのリンクがあります。

TypeScriptでカスタムデータ型を作成して使用する

コンパイラがソースコードに満足しているので、それをどのように改善できるかについて少し考えてみましょう。 このために、私は常に、私たちのドメインにある概念をモデル化することから始めることを提案します。

投稿のカスタムタイプの作成

私たちの店には主に投稿が含まれることがわかっているので、最初のステップは投稿とは何か、そしてそれについて私たちが持っている情報をモデル化することだと思います。 先週、カスタムタイプを作成する方法をすでに見てきたので、今日は投稿のコンセプトを試してみましょう。

 type Post = { id: number; title: string; author: string; day: string; status: string; isSticky: boolean; };

ここに驚きはありませんよね? Postは、数値id 、テキストtitleなど、いくつかの属性を持つオブジェクトです。

Reduxストアが持っているもう1つの重要な情報は、ご想像のとおり、その状態です。 前のセクションでは、それが持つ属性についてすでに説明したので、 Stateタイプの基本的な形状を定義しましょう。

 type State = { posts: any; days: any; };

Stateタイプの改善

これで、 Stateには2つの属性( postsdays )があることがわかりましたが、それらは何でもかまいませんので、それらが何であるかについてはよくわかりanyん。 両方の属性を辞書にしたいと言いました。 つまり、特定のクエリ(投稿のposts IDまたは日のdays )が与えられると、関連データ(それぞれ投稿または投稿のリスト)が必要になります。 オブジェクトを使用して辞書を実装できることはわかっていますが、TypeScriptで辞書を表現するにはどうすればよいでしょうか。

TypeScriptのドキュメントを見ると、かなり一般的な状況に対処するためのいくつかのユーティリティタイプが含まれていることがわかります。 具体的には、必要なタイプのように見えるRecordというタイプがあります。これにより、キーと値のペアを使用して変数を入力できます。キーには特定のKeysタイプがあり、値はTypeです。 このタイプを例に適用すると、次のようになります。

 type State = { posts: Record<number, Post>; days: Record<string, number[]>; };

コンパイラの観点からは、 Record型は、 Keysの任意の値(この例では、 postsnumberdaysstring )が与えられると、その結果は常にTypeのオブジェクト(この場合は、 Postまたはnumber[] 、それぞれ)。 問題は、辞書の動作ではないことです。IDを使用して特定の投稿を検索するときに、関連する投稿が見つかるかどうかをコンパイラに認識させます。つまり、結果はPostになる可能性があります。またはundefined

幸い、これはさらに別のユーティリティタイプであるPartialタイプを使用して簡単に修正できます。

 type State = { posts: Partial< Record<number, Post> >; days: Partial< Record<string, number[]> >; };

タイプエイリアスを使用したコードの改善

私たちの州のposts属性を見てください…何が見えますか? Postタイプの投稿を数字で索引付けする辞書ですね。 ここで、作業中のこのコードを確認している自分を想像してみてください。 このようなタイプに遭遇した場合、インデックス付けされた投稿のnumberはおそらくインデックス付けされた投稿のIDであると思われるかもしれませんが、それは単なる想定です。 コードを確認して確認する必要があります。 そして、 daysどうですか? 「乱数のリストにインデックスを付けるランダムな文字列。」 それはあまり役に立ちませんね。

TypeScript型は、コンパイラチェックのおかげでより堅牢なコードを書くのに役立ちますが、それ以上の方法を提供します。 意味のある型を使用すると、コードがより適切に文書化され、保守が容易になります。 では、既存の型にエイリアスを付けて、意味のある型を作成しましょう。

たとえば、投稿ID( number )と日付( string )がドメインに関連していることがわかっているので、次のタイプエイリアスを簡単に作成できます。

 type PostId = number; type Day = string;

次に、これらのエイリアスを使用して元のタイプを書き直します。

 type Post = { id: PostId; title: string; author: string; day: Day; status: string; isSticky: boolean; }; type State = { posts: Partial< Record<PostId, Post> >; days: Partial< Record<Day, PostId[]> >; };

コードの可読性を向上させるために使用できるもう1つのタイプエイリアスは、 Dictionaryタイプです。これは、便利な構造の背後にPartialRecordを使用する複雑さを「隠し」ます。

 type Dictionary<K extends string | number, T> = Partial< Record<K, T> >;

ソースコードをより明確にする:

 type State = { posts: Dictionary<PostId, Post>; days: Dictionary<Day, PostId[]>; };

以上です! あります! 単純なタイプのエイリアスが3つしかないため、コメントを使用するよりも明らかに優れた方法でコードを文書化することができました。 私たちの後に来る開発者なら誰でも、 postsPostIdを使用してPost型のオブジェクトにインデックスを付ける辞書であり、そのdaysDayを指定すると、リストの投稿識別子を返すデータ構造であることを一目で知ることができます。 あなたが私に尋ねれば、それはかなり素晴らしいです。

しかし、型定義自体が優れているだけでなく、すべてのコードでこれらの新しい型を使用すると、次のようになります。

 function getPost( state: State, id: PostId ): Post | undefined { return state.posts[ id ]; }

また、この新しいセマンティックレイヤーの恩恵も受けています。 ここで、入力したコードの新しいバージョンを確認できます。

ちなみに、型エイリアスは、コンパイラの観点からは、「元の」型と区別がつかないことに注意してください。 これは、たとえば、 PostIdnumberが完全に交換可能であることを意味します。 したがって、 PostIdnumberまたはその逆に割り当てた場合にコンパイラがエラーをトリガーすることを期待しないでください(この小さな例でわかるように)。 それらは単にソースコードにセマンティクスを追加するのに役立ちます。

次のステップ

ご覧のとおり、TypeScriptタイプを使用してJavaScriptコードを段階的に入力できます。そうすることで、品質と読みやすさが向上します。 今日の投稿では、React + Reduxアプリケーションの実際の実装の例を詳細に見ており、比較的少ない労力でどのように改善できるかを見てきました。 しかし、まだ長い道のりがあります。

次の投稿では、現在any型を使用している残りのすべての変数/引数を入力し、いくつかの高度なTypeScriptの機能についても学習します。 この最初の部分が気に入っていただけたら幸いです。気に入った場合は、お友達や同僚と共有してください。

UnsplashのDanielleMacInnesによる注目の画像。