ローカルに @wordpress/interactivity をインストールする

まだ行っていない場合は、TypeScript が IDE でその型を使用できるように、@wordpress/interactivity パッケージをローカルにインストールする必要があります。次のコマンドを使用してこれを行うことができます:

npm install @wordpress/interactivity

そのパッケージを最新の状態に保つことも良い習慣です。

新しい型付きインタラクティブブロックのスキャフォールディング

ローカル環境で TypeScript を使用したインタラクティブブロックの例を探求したい場合は、@wordpress/create-block-interactive-template を使用できます。

まず、コンピュータに Node.js と npm がインストールされていることを確認してください。まだの場合は、Node.js 開発環境 ガイドを確認してください。

次に、@wordpress/create-block パッケージと @wordpress/create-block-interactive-template テンプレートを使用してブロックをスキャフォールディングします。

プラグインを作成したいフォルダーを選択し、そのフォルダー内のターミナルで次のコマンドを実行し、求められたときに typescript バリアントを選択します。

  1. npx @wordpress/create-block@latest --template @wordpress/create-block-interactive-template

重要:ターミナルにスラッグを提供しないでください。そうしないと、create-block がどのバリアントを選択するかを尋ねず、デフォルトで非 TypeScript バリアントが選択されます。

最後に、はじめにガイド の指示に従い続けることができます。残りの指示は同じです。

ストアの型付け

ストアの構造と好みに応じて、ストアの型を生成するために選択できるオプションは3つあります:

  • 1. クライアントストア定義から型を推論します。
  • 2. サーバー状態を手動で型付けしますが、残りはクライアントストア定義から推論します。
  • 3. すべての型を手動で記述します。

1. クライアントストア定義から型を推論する

store 関数を使用してストアを作成すると、TypeScript はストアのプロパティの型を自動的に推論します(stateactionscallbacks など)。これは、通常、単純な JavaScript オブジェクトを書くことで済み、TypeScript が正しい型を判断してくれることを意味します。

カウンターブロックの基本的な例から始めましょう。ブロックの view.ts ファイルにストアを定義し、初期のグローバル状態、アクション、およびコールバックを含めます。

  1. // view.ts
  2. const myStore = store( 'myCounterPlugin', {
  3. state: {
  4. counter: 0,
  5. },
  6. actions: {
  7. increment() {
  8. myStore.state.counter += 1;
  9. },
  10. },
  11. callbacks: {
  12. log() {
  13. console.log( `counter: ${ myStore.state.counter }` );
  14. },
  15. },
  16. } );

myStore の型を TypeScript で検査すると、TypeScript が型を正しく推論できたことがわかります。

  1. const myStore: {
  2. state: {
  3. counter: number;
  4. };
  5. actions: {
  6. increment(): void;
  7. };
  8. callbacks: {
  9. log(): void;
  10. };
  11. };

stateactionscallbacks プロパティを分解しても、型は正しく機能します。

  1. const { state } = store( 'myCounterPlugin', {
  2. state: {
  3. counter: 0,
  4. },
  5. actions: {
  6. increment() {
  7. state.counter += 1;
  8. },
  9. },
  10. callbacks: {
  11. log() {
  12. console.log( `counter: ${ state.counter }` );
  13. },
  14. },
  15. } );

結論として、型を推論することは、store 関数への単一の呼び出しで定義されたシンプルなストアがあり、サーバーで初期化された状態を型付けする必要がない場合に便利です。

2. サーバー状態を手動で型付けし、残りをクライアントストア定義から推論する

wp_interactivity_state 関数でサーバーで初期化されたグローバル状態は、クライアントストア定義には存在せず、したがって手動で型付けする必要があります。しかし、ストアのすべての型を定義したくない場合は、クライアントストア定義の型を推論し、それをサーバーで初期化された状態の型とマージすることができます。

詳細については、サーバーサイドレンダリングガイド を訪れて、wp_interactivity_state とサーバーでのディレクティブの処理方法について学んでください。

前の例に従って、counter 状態の初期化をサーバーに移動しましょう。

  1. wp_interactivity_state( 'myCounterPlugin', array(
  2. 'counter' => 1,
  3. ));

次に、サーバー状態の型を定義し、それをクライアントストア定義から推論された型とマージします。

  1. // Types the server state.
  2. type ServerState = {
  3. state: {
  4. counter: number;
  5. };
  6. };
  7. // Defines the store in a variable to be able to extract its type later.
  8. const storeDef = {
  9. actions: {
  10. increment() {
  11. state.counter += 1;
  12. },
  13. },
  14. callbacks: {
  15. log() {
  16. console.log( `counter: ${ state.counter }` );
  17. },
  18. },
  19. };
  20. // Merges the types of the server state and the client store definition.
  21. type Store = ServerState & typeof storeDef;
  22. // Injects the final types when calling the `store` function.
  23. const { state } = store< Store >( 'myCounterPlugin', storeDef );

または、サーバーで定義された値とクライアントで定義された値の両方を含む全体の状態を型付けすることを気にしない場合は、state プロパティをキャストし、TypeScript にストアの残りを推論させることができます。

クライアントのグローバル状態に product という追加のプロパティがあると仮定しましょう。

  1. type State = {
  2. counter: number; // The server state.
  3. product: number; // The client state.
  4. };
  5. const { state } = store( 'myCounterPlugin', {
  6. state: {
  7. product: 2,
  8. } as State, // Casts the entire state manually.
  9. actions: {
  10. increment() {
  11. state.counter * state.product;
  12. },
  13. },
  14. } );

それでおしまいです。これで、TypeScript はストア定義から actions および callbacks プロパティの型を推論しますが、state プロパティには State 型を使用し、クライアントとサーバーの定義から正しい型を含むようにします。

結論として、このアプローチは、手動で型付けする必要があるサーバー状態がある場合に便利ですが、ストアの残りの型を推論したい場合にも便利です。

3. すべての型を手動で記述する

クライアントストア定義から TypeScript に推論させるのではなく、ストアのすべての型を手動で定義したい場合も可能です。store 関数にそれらを渡すだけです。

  1. // Defines the store types.
  2. interface Store {
  3. state: {
  4. counter: number; // Initial server state
  5. };
  6. actions: {
  7. increment(): void;
  8. };
  9. callbacks: {
  10. log(): void;
  11. };
  12. }
  13. // Pass the types when calling the `store` function.
  14. const { state } = store< Store >( 'myCounterPlugin', {
  15. actions: {
  16. increment() {
  17. state.counter += 1;
  18. },
  19. },
  20. callbacks: {
  21. log() {
  22. console.log( `counter: ${ state.counter }` );
  23. },
  24. },
  25. } );

それでおしまいです!結論として、このアプローチは、ストアのすべての型を制御したい場合に便利であり、手動で記述することを気にしない場合に便利です。

ローカルコンテキストの型付け

初期のローカルコンテキストは、data-wp-context ディレクティブを使用してサーバーで定義されます。

  1. <div data-wp-context='{ "counter": 0 }'>...</div>

そのため、その型を手動で定義し、getContext 関数に渡して、返されるプロパティが正しく型付けされるようにする必要があります。

  1. // Defines the types of your context.
  2. type MyContext = {
  3. counter: number;
  4. };
  5. store( 'myCounterPlugin', {
  6. actions: {
  7. increment() {
  8. // Passes it to the getContext function.
  9. const context = getContext< MyContext >();
  10. // Now `context` is properly typed.
  11. context.counter += 1;
  12. },
  13. },
  14. } );

コンテキストの型を何度も渡す必要がないように、型付き関数を定義し、その関数を getContext の代わりに使用することもできます。

  1. // Defines the types of your context.
  2. type MyContext = {
  3. counter: number;
  4. };
  5. // Defines a typed function. You only have to do this once.
  6. const getMyContext = getContext< MyContext >;
  7. store( 'myCounterPlugin', {
  8. actions: {
  9. increment() {
  10. // Use your typed function.
  11. const context = getMyContext();
  12. // Now `context` is properly typed.
  13. context.counter += 1;
  14. },
  15. },
  16. } );

それでおしまいです!これで、正しい型でコンテキストプロパティにアクセスできます。

派生状態の型付け

派生状態は、グローバル状態またはローカルコンテキストに基づいて計算されるデータです。クライアントストア定義では、state オブジェクトのゲッターを使用して定義されます。

派生状態がインタラクティビティ API でどのように機能するかについて詳しく学ぶには、グローバル状態、ローカルコンテキスト、および派生状態の理解 ガイドを訪れてください。

前の例に従って、カウンターの2倍の派生状態を作成しましょう。

  1. type MyContext = {
  2. counter: number;
  3. };
  4. const myStore = store( 'myCounterPlugin', {
  5. state: {
  6. get double() {
  7. const { counter } = getContext< MyContext >();
  8. return counter * 2;
  9. },
  10. },
  11. actions: {
  12. increment() {
  13. state.counter += 1; // This type is number.
  14. },
  15. },
  16. } );

通常、派生状態がローカルコンテキストに依存している場合、TypeScript は正しい型を推論できます:

  1. const myStore: {
  2. state: {
  3. readonly double: number;
  4. };
  5. actions: {
  6. increment(): void;
  7. };
  8. };

しかし、派生状態の戻り値がグローバル状態の一部に直接依存している場合、TypeScript は循環参照があると主張するため、型を推論できません。

たとえば、この場合、state.double の型は state.counter に依存しており、state の型は state.double の型が定義されるまで完了しないため、循環参照が作成されます。

  1. const { state } = store( 'myCounterPlugin', {
  2. state: {
  3. counter: 0,
  4. get double() {
  5. // TypeScript can't infer this return type because it depends on `state`.
  6. return state.counter * 2;
  7. },
  8. },
  9. actions: {
  10. increment() {
  11. state.counter += 1; // This type is now unknown.
  12. },
  13. },
  14. } );

この場合、TypeScript の設定に応じて、TypeScript は循環参照について警告するか、単に any 型を state プロパティに追加します。

ただし、この問題を解決するのは簡単です。ゲッターの戻り値の型を手動で TypeScript に提供する必要があります。それを行うと、循環参照が消え、TypeScript は再びすべての state 型を推論できるようになります。

  1. const { state } = store( 'myCounterPlugin', {
  2. state: {
  3. counter: 1,
  4. get double(): number {
  5. return state.counter * 2;
  6. },
  7. },
  8. actions: {
  9. increment() {
  10. state.counter += 1; // Correctly inferred!
  11. },
  12. },
  13. } );

これが、前のストアの正しい推論された型です。

  1. const myStore: {
  2. state: {
  3. counter: number;
  4. readonly double: number;
  5. };
  6. actions: {
  7. increment(): void;
  8. };
  9. };

サーバーで wp_interactivity_state を使用する場合は、派生状態の初期値も次のように定義する必要があることを忘れないでください:

  1. wp_interactivity_state( 'myCounterPlugin', array(
  2. 'counter' => 1,
  3. 'double' => 2,
  4. ));

ただし、型を推論している場合は、派生状態の型を手動で定義する必要はありません。すでにクライアントのストア定義に存在します。

  1. // You don't need to type `state.double` here.
  2. type ServerState = {
  3. state: {
  4. counter: number;
  5. };
  6. };
  7. // The `state.double` type is inferred from here.
  8. const storeDef = {
  9. state: {
  10. get double(): number {
  11. return state.counter * 2;
  12. },
  13. },
  14. actions: {
  15. increment() {
  16. state.counter += 1;
  17. },
  18. },
  19. };
  20. // Merges the types of the server state and the client store definition.
  21. type Store = ServerState & typeof storeDef;
  22. // Injects the final types when calling the `store` function.
  23. const { state } = store< Store >( 'myCounterPlugin', storeDef );

それでおしまいです!これで、正しい型で派生状態プロパティにアクセスできます。

非同期アクションの型付け

インタラクティビティ API で TypeScript を使用する際に考慮すべきもう1つのことは、非同期アクションは非同期関数の代わりにジェネレーターで定義する必要があるということです。

インタラクティビティ API の非同期アクションでジェネレーターを使用する理由は、非同期アクションが yield の後に実行を続けるときに、最初にトリガーされたアクションからスコープを復元できるようにするためです。しかし、これは構文の変更だけであり、これらの関数は通常の非同期関数と同様に動作しますstore 関数からの推論された型はこれを反映しています。

前の例に従って、ストアに非同期アクションを追加しましょう。

  1. const { state } = store( 'myCounterPlugin', {
  2. state: {
  3. counter: 0,
  4. get double(): number {
  5. return state.counter * 2;
  6. },
  7. },
  8. actions: {
  9. increment() {
  10. state.counter += 1;
  11. },
  12. *delayedIncrement() {
  13. yield new Promise( ( r ) => setTimeout( r, 1000 ) );
  14. state.counter += 1;
  15. },
  16. },
  17. } );

このストアの推論された型は:

  1. const myStore: {
  2. state: {
  3. counter: number;
  4. readonly double: number;
  5. };
  6. actions: {
  7. increment(): void;
  8. // This behaves like a regular async function.
  9. delayedIncrement(): Promise< void >;
  10. };
  11. };

これにより、外部関数で非同期アクションを使用でき、TypeScript は非同期関数の型を正しく使用します。

  1. const someAsyncFunction = async () => {
  2. // This works fine and it's correctly typed.
  3. await actions.delayedIncrement( 2000 );
  4. };

型を推論せず、ストア全体の型を手動で記述している場合は、非同期アクションのために非同期関数の型を使用できます。

  1. type Store = {
  2. state: {
  3. counter: number;
  4. readonly double: number;
  5. };
  6. actions: {
  7. increment(): void;
  8. delayedIncrement(): Promise< void >; // You can use async functions here.
  9. };
  10. };

非同期アクションを使用する際に考慮すべきことがあります。派生状態と同様に、非同期アクションが値を返す必要があり、この値がグローバル状態の一部に直接依存している場合、TypeScript は循環参照のために型を推論できません。

  1. const { state, actions } = store( 'myCounterPlugin', {
  2. state: {
  3. counter: 0,
  4. },
  5. actions: {
  6. *delayedReturn() {
  7. yield new Promise( ( r ) => setTimeout( r, 1000 ) );
  8. return state.counter; // TypeScript can't infer this return type.
  9. },
  10. },
  11. } );
  12. In this case, just as we did with the derived state, we must manually type the return value of the generator.
  13. const { state, actions } = store( 'myCounterPlugin', {
  14. state: {
  15. counter: 0,
  16. },
  17. actions: {
  18. *delayedReturn(): Generator< uknown, number, uknown > {
  19. yield new Promise( ( r ) => setTimeout( r, 1000 ) );
  20. return state.counter; // Now this is correctly inferred.
  21. },
  22. },
  23. } );
  24. That's it! Remember that the return type of a Generator is the second generic argument: `Generator< unknown, ReturnType, unknown >`.

複数の部分に分割されたストアの型付け

時には、ストアが異なるファイルに分割されることがあります。これは、異なるブロックが同じ名前空間を共有し、各ブロックが必要なストアの部分を読み込む場合に発生します。

2つのブロックの例を見てみましょう:

  • todo-list:TODO リストを表示するブロック。
  • add-post-to-todo:新しい TODO アイテムをリストに追加するボタンを表示するブロックで、テキストは「Read {$post_title}」です。

まず、todo-list ブロックのグローバルおよび派生状態をサーバーで初期化しましょう。

  1. <?php
  2. // todo-list-block/render.php
  3. $todos = array( 'Buy milk', 'Walk the dog' );
  4. wp_interactivity_state( 'myTodoPlugin', array(
  5. 'todos' => $todos,
  6. 'filter' => 'all',
  7. 'filteredTodos' => $todos,
  8. ));
  9. ?>
  10. <!-- HTML markup... -->

次に、サーバー状態を型付けし、クライアントストア定義を追加します。filteredTodos は派生状態なので、手動で型付けする必要はありません。

  1. // todo-list-block/view.ts
  2. type ServerState = {
  3. state: {
  4. todos: string[];
  5. filter: 'all' | 'completed';
  6. };
  7. };
  8. const todoList = {
  9. state: {
  10. get filteredTodos(): string[] {
  11. return state.filter === 'completed'
  12. ? state.todos.filter( ( todo ) => todo.includes( '' ) )
  13. : state.todos;
  14. },
  15. },
  16. actions: {
  17. addTodo( todo: string ) {
  18. state.todos.push( todo );
  19. },
  20. },
  21. };
  22. // Merges the inferred types with the server state types.
  23. export type TodoList = ServerState & typeof todoList;
  24. // Injects the final types when calling the `store` function.
  25. const { state } = store< TodoList >( 'myTodoPlugin', todoList );

今のところ順調です。次に、add-post-to-todo ブロックを作成しましょう。

まず、サーバー状態に現在の投稿タイトルを追加します。

  1. <?php
  2. // add-post-to-todo-block/render.php
  3. wp_interactivity_state( 'myTodoPlugin', array(
  4. 'postTitle' => get_the_title(),
  5. ));
  6. ?>
  7. <!-- HTML markup... -->

次に、そのサーバー状態を型付けし、クライアントストア定義を追加します。

  1. // add-post-to-todo-block/view.ts
  2. type ServerState = {
  3. state: {
  4. postTitle: string;
  5. };
  6. };
  7. const addPostToTodo = {
  8. actions: {
  9. addPostToTodo() {
  10. const todo = `Read: ${ state.postTitle }`.trim();
  11. if ( ! state.todos.includes( todo ) ) {
  12. actions.addTodo( todo );
  13. }
  14. },
  15. },
  16. };
  17. // Merges the inferred types with the server state types.
  18. type Store = ServerState & typeof addPostToTodo;
  19. // Injects the final types when calling the `store` function.
  20. const { state, actions } = store< Store >( 'myTodoPlugin', addPostToTodo );

これはブラウザで正常に動作しますが、TypeScript はこのブロックで stateactionsstate.todosactions.addtodo を含まないと不満を言います。

これを修正するには、TodoList 型を todo-list ブロックからインポートし、他の型とマージする必要があります。

  1. import type { TodoList } from '../todo-list-block/view';
  2. // ...
  3. // Merges the inferred types inferred the server state types.
  4. type Store = TodoList & ServerState & typeof addPostToTodo;

それでおしまいです!これで、TypeScript は state.todosactions.addTodoadd-post-to-todo ブロックで利用可能であることを認識します。

このアプローチにより、add-post-to-todo ブロックは既存の TODO リストと対話しながら、型安全性を維持し、共有ストアに独自の機能を追加できます。

add-post-to-todo 型を todo-list ブロックで使用する必要がある場合は、その型をエクスポートし、他の view.ts ファイルでインポートするだけです。

最後に、型を推論するのではなく、すべての型を手動で定義したい場合は、別のファイルにそれらを定義し、その定義をストアの各部分にインポートできます。TODO リストの例でそれを行う方法は次のとおりです:

  1. // types.ts
  2. interface Store {
  3. state: {
  4. todos: string[];
  5. filter: 'all' | 'completed';
  6. filtered: string[];
  7. postTitle: string;
  8. };
  9. actions: {
  10. addTodo( todo: string ): void;
  11. addPostToTodo(): void;
  12. };
  13. }
  14. export default Store;
  1. // todo-list-block/view.ts
  2. import type Store from '../types';
  3. const { state } = store< Store >( 'myTodoPlugin', {
  4. // Everything is correctly typed here
  5. } );
  1. // add-post-to-todo-block/view.ts
  2. import type Store from '../types';
  3. const { state, actions } = store< Store >( 'myTodoPlugin', {
  4. // Everything is correctly typed here
  5. } );

このアプローチにより、型を完全に制御でき、ストアのすべての部分で一貫性を確保できます。これは、複雑なストア構造がある場合や、複数のブロックやコンポーネントにわたって特定のインターフェースを強制したい場合に特に便利です。

型付きストアのインポートとエクスポート

インタラクティビティ API では、他の名前空間のストアに store 関数を使用してアクセスできます。

todo-list ブロックの例に戻りましょうが、今回は add-post-to-todo ブロックが別のプラグインに属し、したがって異なる名前空間を使用すると想像してみましょう。

  1. // Import the store of the `todo-list` block.
  2. const myTodoPlugin = store( 'myTodoPlugin' );
  3. store( 'myAddPostToTodoPlugin', {
  4. actions: {
  5. addPostToTodo() {
  6. const todo = `Read: ${ state.postTitle }`.trim();
  7. if ( ! myTodoPlugin.state.todos.includes( todo ) ) {
  8. myTodoPlugin.actions.addTodo( todo );
  9. }
  10. },
  11. },
  12. } );

これはブラウザで正常に動作しますが、TypeScript は myTodoPlugin.statemyTodoPlugin.actions が型付けされていないと不満を言います。

これを修正するには、myTodoPlugin プラグインが正しい型で store 関数を呼び出した結果をエクスポートし、それをスクリプトモジュールを使用して利用可能にすることができます。

  1. // Export the already typed state and actions.
  2. export const { state, actions } = store< TodoList >( 'myTodoPlugin', {
  3. // ...
  4. } );

これで、add-post-to-todo ブロックは myTodoPlugin スクリプトモジュールから型付きストアをインポートでき、ストアが読み込まれるだけでなく、正しい型も含まれていることが保証されます。

  1. import { store } from '@wordpress/interactivity';
  2. import {
  3. state as todoState,
  4. actions as todoActions,
  5. } from 'my-todo-plugin-module';
  6. store( 'myAddPostToTodoPlugin', {
  7. actions: {
  8. addPostToTodo() {
  9. const todo = `Read: ${ state.postTitle }`.trim();
  10. if ( ! todoState.todos.includes( todo ) ) {
  11. todoActions.addTodo( todo );
  12. }
  13. },
  14. },
  15. } );

my-todo-plugin-module スクリプトモジュールを依存関係として宣言する必要があることを忘れないでください。

他のストアがオプションであり、早期に読み込みたくない場合は、静的インポートの代わりに動的インポートを使用できます。

  1. import { store } from '@wordpress/interactivity';
  2. store( 'myAddPostToTodoPlugin', {
  3. actions: {
  4. *addPostToTodo() {
  5. const todoPlugin = yield import( 'my-todo-plugin-module' );
  6. const todo = `Read: ${ state.postTitle }`.trim();
  7. if ( ! todoPlugin.state.todos.includes( todo ) ) {
  8. todoPlugin.actions.addTodo( todo );
  9. }
  10. },
  11. },
  12. } );

結論

このガイドでは、型を自動的に推論することから手動で定義することまで、インタラクティビティ API ストアの型付けに関するさまざまなアプローチを探求しました。また、サーバー初期化状態、ローカルコンテキスト、派生状態の処理方法や、非同期アクションの型付けについても説明しました。

型を推論するか手動で定義するかの選択は、特定のニーズとストアの複雑さに依存することを忘れないでください。どのアプローチを選んでも、TypeScript はより良く、より信頼性の高いインタラクティブブロックを構築するのに役立ちます。