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

公開: 2020-11-20

TypeScriptチュートリアルを続行する(そしてできれば終了する)時が来ました。 TypeScriptについて書いた以前の投稿を見逃した場合は、次のようになります。TypeScriptの最初の紹介と、このチュートリアルの最初の部分で、使用しているJavaScriptの例とそれを部分的に改善するために行った手順について説明します。 。

今日は、まだ不足しているすべてを完了することによって、例を終了します。 具体的には、最初に、他の既存のタイプの部分的なバージョンであるタイプを作成する方法を確認します。 次に、型共用体を使用してReduxストアのアクションを正しく入力する方法を確認し、型共用体が提供する利点について説明します。 そして最後に、戻り値の型が引数に依存するポリモーフィック関数を作成する方法を示します。

これまでに行ったことの簡単なレビュー…

チュートリアルの最初の部分では、作業例としてNelio Contentから取得したReduxストア(の一部)を使用しました。 それはすべてプレーンなJavaScriptコードとして始まりましたが、より堅牢でわかりやすいように具体的な型を追加することで改善する必要がありました。 したがって、たとえば、次のタイプを定義しました。

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

これは、私たちの店が扱っている情報の種類を一目で理解するのに役立ちました。 この特定の例では、たとえば、アプリケーションの状態に2つのものが格納されていることがわかります。 postsのリスト( PostIdを介してインデックス付けされています)と、特定のdaysを指定すると、投稿識別子。 Postオブジェクトにある属性(およびそれらの特定のタイプ)も確認できます。

これらのタイプが定義されたら、それらを使用するために例のすべての関数を編集しました。 この単純なタスクは、JavaScriptの不透明な関数シグネチャを変換しました。

 // Selectors function getPost( state, id ) { ... } function getPostsInDay( state, day ) { ... } // Actions function receiveNewPost( post ) { ... } function updatePost( postId, attributes ) { ... } // Reducer function reducer( state, action ) { ... }

自明のTypeScript関数シグネチャへ:

 // Selectors function getPost( state: State, id: PostId ): Post | undefined { ... } function getPostsInDay( state: State, day: Day ): PostId[] { ... } // Actions function receiveNewPost( post: Post ): any { ... } function updatePost( postId: PostId, attributes: any ): any { ... } // Reducer function reducer( state: State, action: any ): State { ... }

getPostsInDay関数は、TypeScriptがコードの品質をどれだけ向上させるかを示す非常に良い例です。 JavaScriptの対応物を見ると、その関数が何を返すのか本当にわかりません。 確かに、その名前は結果の種類を示唆している可能性があります(おそらく投稿のリストですか?)が、関数のソースコード(そしておそらくアクションとレデューサーも)を見て(実際には投稿ID)。 ものに適切な名前を付けることでこの状況を改善できますが(たとえば、 getIdsOfPostsInDay )、疑いを取り除く具体的な型のようなものはありません: PostId[]

さて、あなたは現在の状況に精通しているので、先週スキップしたすべてを修正する時が来ました。 具体的には、 updatePost関数の属性attributesを入力する必要があり、アクションの型を定義する必要があることがわかっています( reducerでは、現在のaction属性はany型であることに注意してください)。

属性が別のオブジェクトのサブセットであるオブジェクトを入力する方法

簡単なことから始めてウォームアップしましょう。 updatePost関数は、特定の投稿IDの特定の属性を更新する意図を示すアクションを生成します。 外観は次のとおりです。

 function updatePost( postId: PostId, attributes: any ): any { return { type: 'UPDATE_POST', postId, attributes, }; }

レデューサーがアクションを使用してストアの投稿を更新する方法は次のとおりです。

 function reducer( state: State, action: any ): State { // ... switch ( action.type ) { // ... case 'UPDATE_POST': if ( ! state.posts[ action.postId ] ) { return state; } const post = { ...state.posts[ action.postId ], ...action.attributes, }; return { ... }; } // ... }

ご覧のとおり、レデューサーはストア内の投稿を検索し、そこにある場合は、アクションに含まれている属性を使用して属性を上書きすることにより、属性を更新します。

しかし、アクションのattributesは正確には何ですか? まあ、それらは明らかにPostに似たものです。投稿で見つかる可能性のある属性を上書きすることになっているからです。

 type UpdatePostAction = { type: 'UPDATE_POST'; postId: number; attributes: Post; };

しかし、これを使おうとすると、機能しないことがわかります。

 const post: Post = { id: 1, title: 'Title', author: 'Ruth', day: '2020-10-01', status: 'draft', isSticky: false, }; const action: UpdatePostAction = { type: 'UPDATE_POST', postId: 1, attributes: { author: 'Toni', }, };

attributesPost自体にしたくないからです。 Post属性のサブセットにします(つまり、上書きするPostオブジェクトの属性のみを指定します)。

この問題を解決するには、 Partialユーティリティタイプを使用します。

 type UpdatePostAction = { type: 'UPDATE_POST'; postId: number; attributes: Partial<Post>; };

以上です! またはそれは?

属性を明示的にフィルタリングする

TypeScriptのコンパイラがチェックしていないランタイムエラーが発生する可能性があるため、前のスニペットはまだ欠陥があります。 その理由は次のとおりです。更新後を通知するアクションには、2つの引数、投稿IDと更新する属性のセットがあります。 アクションの準備ができたら、レデューサーは既存の投稿を新しい値で上書きします。

 const post = { ...state.posts[ action.postId ], ...action.attributes, };

そして、それはまさに私たちのコードの欠陥部分です。 アクションのpostId属性にポットIDxがあり、属性のid attributesに異なる投稿IDyがある可能性があります。

 const action: UpdatePostAction = { type: 'UPDATE_POST', postId: 1, attributes: { id: 2, author: 'Toni', }, };

これは明らかに有効なアクションであるため、TypeScriptはエラーをトリガーしませんが、トリガーされるべきではないことはわかっています。 属性のid attributes (存在する場合)とpostId属性は同じ値である必要があります。そうでない場合、一貫性のないアクションが発生します。 不可能な状況を定義できるため、アクションタイプは不正確です…では、どうすればこれを修正できますか? 非常に簡単です。このタイプを変更するだけで、不可能であるはずのこのシナリオが実際に不可能になります。

私が考えた最初の解決策は次のとおりです。アクションからpostId属性を削除し、 attributes属性にIDを追加します。

 type UpdatePostAction = { type: 'UPDATE_POST'; attributes: Partial<Post>; }; function updatePost( postId: PostId, attributes: Partial<Post> ): UpdatePostAction { return { type: 'UPDATE_POST', attributes: { ...attributes, id: postId, }, }; }

次に、レデューサーを更新して、 action.attributes.idではなくaction.postIdを使用して、既存の投稿を検索して上書きします。

残念ながら、 attributesは「部分的な投稿」であるため、このソリューションは理想的ではありません。覚えていますか? これは、理論的には、 id属性がattributesオブジェクトに含まれる場合と含まれない場合があることを意味します。 確かに、私たちはアクションを生成しているので、それがそこにあることを知っています…しかし、私たちのタイプはまだ不正確です。 将来誰かがupdatePost関数を変更し、 attributespostIdが含まれていることを確認しない場合、結果のアクションはTypeScriptに従って有効になりますが、コードは機能しません。

 const workingAction: UpdatePostAction = { type: 'UPDATE_POST', attributes: { id: 1, author: 'Toni', }, }; const failingAction: UpdatePostAction = { type: 'UPDATE_POST', attributes: { author: 'Toni', }, };

したがって、TypeScriptで保護する場合は、型を指定するときにできるだけ正確にし、不可能な状態を不可能にするようにする必要があります。 これらすべてを考慮すると、利用できるオプションは2つだけです。

  1. postId属性が動作している場合(最初に行ったように)、 attributesオブジェクトにid属性を含めることはできません
  2. 一方、アクションにpostId属性がない場合、 attributesにはid属性が含まれている必要があります。

最初のソリューションは、別のユーティリティタイプであるOmitを使用して簡単に指定できます。これにより、既存のタイプから属性を削除して、新しいタイプを作成できます。

 type UpdatePostAction = { type: 'UPDATE_POST'; postId: PostId, attributes: Partial< Omit<Post, 'id'> >; };

これは期待どおりに機能します:

 const workingAction: UpdatePostAction = { type: 'UPDATE_POST', postId: 1, attributes: { author: 'Toni', }, }; const failingAction: UpdatePostAction = { type: 'UPDATE_POST', postId: 1, attributes: { id: 1, author: 'Toni', }, };

2番目のオプションでは、定義したPartial<Post>タイプの上にid属性を明示的に追加する必要があります。

 type UpdatePostAction = { type: 'UPDATE_POST'; attributes: Partial<Post> & { id: PostId }; };

これもまた、期待される結果をもたらします。

 const workingAction: UpdatePostAction = { type: 'UPDATE_POST', attributes: { id: 1, author: 'Toni', }, }; const failingAction: UpdatePostAction = { type: 'UPDATE_POST', attributes: { author: 'Toni', }, };

共用体の種類

前のセクションでは、ストアの2つのアクションのいずれかを入力する方法をすでに見てきました。 2番目のアクションでも同じことをしましょう。 receiveNewPostが次のようになっていることを知っています。

 function receiveNewPost( post: Post ): any { return { type: 'RECEIVE_NEW_POST', post, }; }

そのタイプは次のように定義できます。

 type ReceiveNewPostAction = { type: 'RECEIVE_NEW_POST'; post: Post; };

簡単ですよね?

次に、レデューサーを見てみましょう。これは、 stateaction (タイプはまだわかりません)を取り、新しいStateを生成します。

 function reducer( state: State, action: any ): State { ... }

当店には、 UpdatePostActionReceiveNewPostActionの2種類のアクションがあります。 では、 action引数のタイプは何ですか? どちらかですよね? 変数が複数の型ABCなどを受け入れることができる場合、その型は型の共用体です。 つまり、そのタイプはAB 、またはCのいずれかになります。 共用体型は、その共用体で指定された任意の型の値をとることができる型です。

Actionタイプを共用体タイプとして定義する方法は次のとおりです。

 type Action = UpdatePostAction | ReceiveNewPostAction;

前のスニペットは、 ActionUpdatePostActionタイプのインスタンスまたはReceiveNewPostActionタイプのインスタンスのいずれかである可能性があることを単に示しています。

レデューサーでActionを使用する場合:

 function reducer( state: State, action: Action ): State { ... }

よく型付けされたこの新しいバージョンのコードベースがどのようにスムーズに機能するかを確認できます。

共用体型がデフォルトのケースを排除する方法

「ちょっと待ってください」とあなたは言うかもしれません、「前のリンクはスムーズに機能していません、コンパイラはエラーを引き起こしています!」 実際、TypeScriptによると、レデューサーには到達不能コードが含まれています。

 function reducer( state: State, action: Action ): State { // ... switch ( action.type ) { case 'RECEIVE_NEW_POST': // ... case 'UPDATE_POST': // ... } return state; //Error! Unreachable code }

待って、何? ここで何が起こっているのか説明させてください…

作成したAction共用体タイプは、実際には識別された共用体タイプです。 識別された共用体型は、そのすべての型が共通の属性を共有する共用体型であり、その値を使用して1つの型を他の型から区別することができます。

この場合、2つのActionタイプには、 ReceiveNewPostActionの場合はRECEIVE_NEW_POSTUpdatePostActionの場合はUPDATE_POSTの値を持つtype属性があります。 Actionは必然的にいずれかのアクションのインスタンスであることがわかっているため、 switchの2つのブランチは、すべての可能性をカバーしますaction.typeRECEIVE_NEW_POSTであるか、 UPDATE_POSTです。 したがって、最終的なreturnは冗長であり、到達できなくなります。

次に、このエラーを修正するためにそのreturnを削除するとします。 不要なコードを削除する以外に、何かを得ましたか? 答えはイエスです。 コードに新しいタイプのアクションを追加すると、次のようになります。

 type Action = | UpdatePostAction | ReceiveNewPostAction | NewFeatureAction; type NewFeatureAction = { type: 'NEW_FEATURE'; // ... };

突然、レデューサーのswitchステートメントは、考えられるすべてのシナリオをカバーしなくなります。

 function reducer( state: State, action: Action ): State { // ... switch ( action.type ) { case 'RECEIVE_NEW_POST': // ... case 'UPDATE_POST': // ... // case NEW_FEATURE is missing... } // return undefined is now implicit }

これは、タイプNEW_FEATUREのアクションを使用してレデューサーを呼び出すと、レデューサーが暗黙的にundefinedの値を返す可能性があることを意味します。これは、関数のシグネチャと一致しません。 この不一致のために、TypeScriptは文句を言い、この新しいアクションタイプを処理するための新しいブランチがないことを知らせます。

可変戻り型を持つポリモーフィック関数

ここまで進んだら、おめでとうございます。TypeScriptを使用してJavaScriptアプリケーションのソースコードを改善するために必要なすべてのことを学びました。 そして、ご褒美として、数日前に遭遇した「問題」とその解決策をお伝えします。 なんで? TypeScriptは複雑で魅力的な世界であり、これがどの程度まで当てはまるかをお見せしたいと思います。

この冒険全体の初めに、私たちが持っているセレクターの1つがgetPostsInDayであり、その戻りタイプが投稿IDのリストであることがわかりました。

 function getPostsInDay( state: State, day: Day ): PostId[] { return state.days[ day ] ?? []; }

名前が示すように、投稿のリストが返される場合があります。 なぜ私はそのような誤解を招くような名前を使用したのですか、あなたは疑問に思っていますか? 次のシナリオを想像してみてください。引数の1つの値に応じて、この関数が投稿IDのリストを返すか、実際の投稿のリストを返すことができるようにしたいとします。 このようなもの:

 const ids: PostId[] = getPostsInDay( '2020-10-01', 'id' ); const posts: Post[] = getPostsInDay( '2020-10-01', 'all' );

TypeScriptでそれを行うことはできますか? もちろんそうです! 他になぜ私はこれを他の方法で持ち出すのでしょうか? 私たちがしなければならないのは、結果が入力パラメーターに依存するポリモーフィック関数を定義することだけです。

したがって、同じ関数の2つの異なるバージョンが必要です。 属性の1つがstring idである場合は、 PostIdのリストを返す必要があります。 同じ属性がstring allの場合、もう一方はPostのリストを返す必要があります。

両方を作成しましょう:

 function getPostsInDay( state: State, day: Day, mode: 'id' ): PostId[] { // ... } function getPostsInDay( state: State, day: Day, mode: 'all' ): Post[] { // ... }

簡単ですよね? 違う! これは機能しません。 TypeScriptによると、「重複関数の実装」があります。

では、別のことを試してみましょう。 前の2つの定義を1つの関数にマージしてみましょう。

 function getPostsInDay( state: State, day: Day, mode: 'id' | 'all' = 'id' ): PostId[] | Post[] { if ( 'id' === mode ) { return state.days[ day ] ?? []; } return []; }

これは私たちが望むように動作しますか? そうではないのではないかと思います…

この関数シグニチャは次のように示しています。「 getPostsInDayは、 statemodeの2つの引数をとる関数であり、値はidまたはallのいずれかになります。 その戻りタイプは、 PostIdのリストまたはPostのリストのいずれかになります。」 言い換えると、前の関数定義では、 mode引数に指定された値と関数の戻り型の間に関係があることをどこにも指定していません。 したがって、次のようなコードを作成します。

 const state: State = { posts: {}, days: {} }; const ids: PostId[] = getPostsInDay( state, '2020-10-01', 'id' ); const posts: Post[] = getPostsInDay( state, '2020-10-01', 'all' );

は有効であり、希望どおりに動作しません。

OK、最後の試み。 具体的な関数シグネチャを記述する最初の直感と、単一の有効な実装を組み合わせるとどうなるでしょうか。

 function getPostsInDay( state: State, day: Day, mode: 'id' ): PostId[]; function getPostsInDay( state: State, day: Day, mode: 'all' ): Post[]; function getPostsInDay( state: State, day: Day, mode: 'id' | 'all' ): PostId[] | Post[] { const postIds = state.days[ day ] ?? []; if ( 'id' === mode ) { return postIds; } return postIds .map( ( pid ) => getPost( state, pid ) ) .filter( ( p ): p is Post => !! p ); }

前のスニペットには、機能する有効な関数実装がありますが、 mode内の具象値を関数の戻り型にバインドする2つの追加の関数シグネチャを定義しています。

このアプローチを使用すると、このコードは有効です。

 const ids: PostId[] = getPostsInDay( state, '2020-10-01', 'id' ); const posts: Post[] = getPostsInDay( state, '2020-10-01', 'all' );

そしてこれはしません:

 const ids: PostId[] = getPostsInDay( state, '2020-10-01', 'all' ); const posts: Post[] = getPostsInDay( state, '2020-10-01', 'id' );

結論

この一連の投稿では、TypeScriptとは何か、そしてそれをプロジェクトにどのように適用できるかを見てきました。 タイプは、セマンティックコンテキストを提供することにより、コードをより適切に文書化するのに役立ちます。 さらに、TypeScriptコンパイラは、レゴと同じように、コードが正しく組み合わされていることを検証するため、型によってセキュリティの層が追加されます。

この時点で、作業の品質を次のレベルに引き上げるために必要なすべてのツールがすでに用意されています。 この新しい冒険で頑張ってください!

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