TypeScriptを@wordpress/dataストアに追加する
公開: 2021-02-05昨年、TypeScriptについて多くのことを話しました。 私の最新の投稿の1つで、実際の例を通じてWordPressプラグインでTypeScriptを使用する方法、特にセレクター、アクション、レデューサーにタイプを追加してReduxストアを改善する方法を見ました。
この例では、次のような基本的なJavaScriptコードを使用しています。
// Selectors function getPost( state, id ) { … } function getPostsInDay( state, day ) { … } // Actions function receiveNewPost( post ) { … } function updatePost( postId, attributes ) { … } // Reducer function reducer( state, action ) { … }ここで、各関数が何をするのか、そして各パラメーターが何であるのかについての手がかりを私たちに与えた唯一のものは、私たちの命名能力に依存します。
// Selectors function getPost( state: State, id: PostId ): Post | undefined { … } function getPostsInDay( state: State, day: Day ): PostId[] { … } // Actions function receiveNewPost( post: Post ): ReceiveNewPostAction { … } function updatePost( postId: PostId, attributes: Partial<Post> ): UpdatePostAction { … } // Reducer function reducer( state: State, action: Action ): State { … }すべてが適切に入力されるため、すべてがより明確になります。
type PostId = number; type Day = string; type Post = { id: PostId; title: string; author: string; day: Day; status: string; isSticky: boolean; }; type State = { posts: Dictionary; days: Dictionary; };数週間前、私は新しいプラグインであるNelio Unlockerに取り組んでいましたが、これらすべての手法を適用するときに問題が発生しました。 それでは、前述の問題を確認して、それを克服する方法を学びましょう!
問題
ご存知かもしれませんが、ストアで定義したセレクターやアクションを使用する場合は、Reactフック( useSelectおよびuseDispatchを使用)または上位コンポーネント( withSelectおよびwithDispatchを使用)を介してアクセスします。 、これらはすべて@wordpress/dataパッケージによって提供されます。
たとえば、今見たgetPostセレクターとupdatePostアクションを使用したい場合は、次のようにする必要があります(ストアの名前がnelio-storeであると仮定します)。
const Component = ( { postId } ): JSX.Element => { const post = useSelect( ( select ): Post => select( 'nelio-store' ).getPost( postId ); ); const { updatePost } = useDispatch( 'nelio-store' ); return ( ... ); };前のスニペットでは、Reactフックを使用してセレクターとアクションにアクセスしていることがわかります。 しかし、TypeScriptは、そのタイプが何であるかは言うまでもなく、それらのセレクターとアクションが存在することをどのように知っていますか?
まあ、それはまさに私が直面した問題です。 つまり、 select('nelio-store')にアクセスした結果は、すべてのストアセレクターを含むオブジェクトであり、 dispatch('nelio-store')は、ストアアクションを含むオブジェクトであることをTypeScriptに伝える方法を知りたいと思いました。 。
ソリューション
TypeScriptに関する前回の投稿では、ポリモーフィック関数について説明しました。 ポリモーフィック関数を使用すると、指定された引数に基づいてさまざまな戻り値の型を指定できます。 TypeScriptポリモーフィズムを使用して、ストアの名前をパラメーターとして@wordpress/dataパッケージのselectメソッドまたはdispatchメソッドを呼び出すと、それぞれセレクターとアクションが得られるように指定できます。
これを行うには、次のようにストアを登録するファイルにdeclare moduleブロックを追加するだけです。
// WordPress dependencies import { registerStore } from '@wordpress/data'; import { controls } from '@wordpress/data-controls'; // Internal dependencies import reducer from './reducer'; import * as actions from './actions'; import * as selectors from './selectors'; const STORE = 'nelio-store'; registerStore( STORE, { controls, reducer, actions, selectors, } ); // Extend @wordpress/data with our store declare module '@wordpress/data' { function select( key: typeof STORE ): Selectors; function dispatch( key: typeof STORE ): Actions; } 次に、 SelectorsとActionsのタイプが実際に何であるかを定義します。
type Selectors = { getPost: ( id: PostId ) => Post | undefined; getPostsInDay: ( day: Day ) => PostId[]; } type Actions = { receiveNewPost: ( post: Post ) => void; updatePost: ( postId: PostId, attributes: Partial<Post> ) => void; } これまでのところ、とても良いですよね? 唯一の「問題」は、 SelectorsとActionsのタイプを手動で定義する必要があることです。これは、TypeScriptが適切にタイプされたselectorsとactionsのセットがあることをすでに知っていることを考えると、奇妙に聞こえます…
TypeScriptで関数型を操作する
インポートしたactionsとselectorsオブジェクトのタイプを見ると、TypeScriptが次のことを示していることがわかります。
typeof selectors === { getPost: ( state: State, id: PostId ) => Post | undefined; getPostsInDay: ( state: State, day: Day ) => PostId[]; } typeof actions === { receiveNewPost: ( post: Post ) => ReceiveNewPostAction; updatePost: ( postId: PostId, attributes: Partial<Post> ) => UpdatePostAction; } ご覧のとおり、それらのタイプは、前のセクションで手動で定義したタイプの正確なコピーです。 ほぼ正確です。セレクターには最初の引数がなく(ストアstate selectからセレクターを呼び出すと存在しないため)、アクションはvoidを返します( dispatchを介して呼び出されたアクションは何も返さないため)。
それらを使用して、必要なSelectorsとActionsのタイプを自動的に生成できますか?
TypeScriptで関数型の最初のパラメーターを削除する方法
getPostセレクターに少し焦点を当てましょう。 そのタイプは次のとおりです。
// Old type typeof getPost === ( state: State, id: PostId ) => Post | undefined 先ほど述べたように、 stateパラメーターを持たない新しい関数型が必要です。
// New type ( id: PostId ) => Post | undefinedしたがって、既存の型から新しい型を生成するには、TypeScriptが必要です。 これは、言語のいくつかの高度な機能を組み合わせることで実現できます。
type OmitFirstArg< F > = F extends ( x: any, ...args: infer P ) => infer R ? ( ...args: P ) => R : never;複雑ですねここで何が起こっているのかを詳しく見てみましょう。

-
type OmitFirstArg<F>と入力します。 まず、新しい補助ジェネリック型(OmitFirstArg)を定義します。 一般に、ジェネリック型は、既存の型から新しい型を定義できる型です。 たとえば、Array<T>タイプは、物事のリストを作成できるので、おそらくご存知でしょうArray<string>は文字列のリスト、Array<Post>はPostのリストなどです。この概念、OmitFirstArg<F>は、関数の最初の引数を削除するヘルパー型です。 - これはジェネリック型であるため、理論的には他のTypeScript型で使用できます。 つまり、
OmitFirstArg<string>やOmitFirstArg<Post>のようなものが可能です…このタイプは、少なくとも1つの引数を持つ関数でのみ使用する必要があることがわかっています。 このヘルパー型が関数でのみ使用されるようにするために、条件付き型としてのみ定義します。 条件付き型では、条件に基づいて結果の型を指定します。「Fが少なくとも1つの引数(条件)を持つ関数である場合、結果の型は最初の引数が削除された別の関数です(条件がtrue); それ以外の場合は、neverタイプを使用します(条件がfalseの場合にタイプします)。」 -
F extends XXXを拡張します。 条件を指定する式です。Fが文字列であることを確認しますか? 次のように入力するだけですF extends string。 簡単に簡単。 しかし、「1つの引数を持つ関数」についてはどうでしょうか。 それは確かにもっと複雑に聞こえます… -
(x: any, ...args: infer P) => infer R。 これは関数型です。引数(括弧内)で始まり、矢印が続き、関数の戻り型が続きます。 この特定のケースでは、関数に1つの引数x(特定の型は関係ありません)が必要です。 この型定義には2つの興味深いビットがあります。 一方では、rest演算子を使用して、残りのargsのタイプP(存在する場合)をキャプチャします。 一方、TypeScriptの型推論(infer)を使用して、これらの型Pが実際に何であるか、および正確な戻り型Rを把握します。 -
? (...args: P) => R : never? (...args: P) => R : never。 最後に、条件付きタイプを完成させます。Fが関数の場合、戻り型は、引数がP型で、戻り型がRである新しい関数です。 そうでない場合、戻りタイプはneverではありません。
このヘルパータイプを使用して、必要な新しいタイプを作成する方法は次のとおりです。
const getPost = ( state: State, id: PostId ) => Post | undefined; OmitFirstArg< typeof getPost > === ( id: PostId ) => Post | undefined;そして、私たちはすでに私たちが望むものを達成することに一歩近づいています! ここでは、この例を遊び場で見ることができます。
TypeScriptで関数型の戻り型を変更する方法
あなたはすでに知っていることで答えを知っていると確信しています:関数型を取り、新しい関数型を返す補助ジェネリック型が必要です。 このようなもの:
type RemoveReturnType< F > = F extends ( ...args: infer P ) => any ? ( ...args: P ) => void : never; 簡単ですよね? これは、前のセクションで行ったことと非常によく似ていますPのargsの型をキャプチャし(今回は少なくとも1つの引数xを要求する必要はありません)、戻り型を無視します。 Fが関数の場合、 voidを返す新しい関数を返します。 それ以外の場合は、 never戻りません。 素晴らしい!
遊び場でこれをチェックしてください。
TypeScriptでオブジェクトタイプを別のオブジェクトタイプにマップする方法
アクションとセレクターは2つのオブジェクトであり、そのキーはそれらのアクションとセレクターの名前であり、値は関数自体です。 これは、これらのオブジェクトのタイプが次のようになることを意味します。
typeof selectors === { getPost: ( state: State, id: PostId ) => Post | undefined; getPostsInDay: ( state: State, day: Day ) => PostId[]; } typeof actions === { receiveNewPost: ( post: Post ) => ReceiveNewPostAction; updatePost: ( postId: PostId, attributes: Partial<Post> ) => UpdatePostAction; }前の2つのセクションでは、あるタイプの関数を別のタイプの関数に変換する方法を学びました。 これは、次のように手動で新しい型を定義できることを意味します。
type Selectors = { getPost: OmitFirstArg< typeof selectors.getPost >; getPostsInDay: OmitFirstArg< typeof selectors.getPostsInDay >; }; type Actions = { receiveNewPost: RemoveReturnType< actions.receiveNewPost >; updatePost: RemoveReturnType< actions.updatePost >; }; しかし、もちろん、これは長期的には持続可能ではありません。両方のタイプの関数の名前を手動で指定しています。 明らかに、 actionsとselectorsの元の型定義を新しい型に自動的にマップしたいと考えています。
TypeScriptでこれを行う方法は次のとおりです。
type OmitFirstArgs< O > = { [ K in keyof O ]: OmitFirstArg< O[ K ] >; } type RemoveReturnTypes< O > = { [ K in keyof O ]: RemoveReturnType< O[ K ] >; }うまくいけば、これはすでに理にかなっていますが、とにかく前のスニペットが行うことをすぐに解き放ちましょう:
-
type OmitFirstArgs<O>と入力します。 オブジェクトOをとる新しい補助ジェネリック型を作成します。 - 結果は別のオブジェクトタイプになります(中括弧が
{...}を示すように)。 -
[K in keyof O]。 新しいオブジェクトが持つ正確なキーはわかりませんが、Oに含まれているキーと同じキーである必要があることはわかっています。 これがTypeScriptに伝えることです。つまり、keyof OのキーであるすべてのキーKが必要です。 - そして、キー
Kごとに、そのタイプはOmitFirstArg<O[K]>です。 つまり、元のタイプ(O[K])を取得し、定義した補助タイプ(この場合はOmitFirstArg)を使用して必要なタイプに変換します。 - 最後に、
RemoveReturnTypesと元の補助タイプRemoveReturnTypeでも同じことを行います。
セレクターとアクションを使用して@wordpress/dataを拡張する
今日見た4つの補助タイプをglobal.d.tsファイルに追加し、それをプロジェクトのルートに保存すると、この投稿で見たすべてを最終的に組み合わせて、元の問題を解決できます。
// WordPress dependencies import { registerStore } from '@wordpress/data'; import { controls } from '@wordpress/data-controls'; // Internal dependencies import reducer from './reducer'; import * as actions from './actions'; import * as selectors from './selectors'; // Types type Selectors = OmitFirstArgs< typeof selectors >; type Actions = RemoveReturnTypes< typeof actions >; const STORE = 'nelio-store'; registerStore( STORE, { controls, reducer, actions, selectors, } ); // Extend @wordpress/data with our store declare module '@wordpress/data' { function select( key: typeof STORE ): Selectors; function dispatch( key: typeof STORE ): Actions; }以上です! この開発のヒントが気に入っていただけたら幸いです。気に入った場合は、同僚や友人と共有してください。 おー! また、同じ結果を得るための別のアプローチを知っている場合は、コメントで教えてください。
UnsplashのGabrielCrismariuによる注目の画像。
