## 导入和导出类型化存储

在交互性 API 中，可以使用 `store` 函数访问来自其他命名空间的存储。

让我们回到 `todo-list` 区块示例，但这次假设 `add-post-to-todo` 区块属于不同的插件，因此将使用不同的命名空间。

```ts
// 导入 `todo-list` 区块的存储
const myTodoPlugin = store( 'myTodoPlugin' );

store( 'myAddPostToTodoPlugin', {
	actions: {
		addPostToTodo() {
			const todo = `阅读：${ state.postTitle }`.trim();
			if ( ! myTodoPlugin.state.todos.includes( todo ) ) {
				myTodoPlugin.actions.addTodo( todo );
			}
		},
	},
} );
```

这在浏览器中运行正常，但 TypeScript 会提示 `myTodoPlugin.state` 和 `myTodoPlugin.actions` 未定义类型。

要解决这个问题，`myTodoPlugin` 插件可以导出带有正确类型调用的 `store` 函数结果，并通过脚本模块使其可用。

```ts
// 导出已定义类型的状态和操作
export const { state, actions } = store< TodoList >( 'myTodoPlugin', {
	// ...
} );
```

现在，`add-post-to-todo` 区块可以从 `myTodoPlugin` 脚本模块导入类型化存储，这不仅确保存储会被加载，还保证其包含正确的类型。

```ts
import { store } from '@wordpress/interactivity';
import {
	state as todoState,
	actions as todoActions,
} from 'my-todo-plugin-module';

store( 'myAddPostToTodoPlugin', {
	actions: {
		addPostToTodo() {
			const todo = `阅读：${ state.postTitle }`.trim();
			if ( ! todoState.todos.includes( todo ) ) {
				todoActions.addTodo( todo );
			}
		},
	},
} );
```

请记住，您需要将 `my-todo-plugin-module` 脚本模块声明为依赖项。

如果其他存储是可选的，并且您不希望急切加载它，可以使用动态导入代替静态导入。

```ts
import { store } from '@wordpress/interactivity';

store( 'myAddPostToTodoPlugin', {
	actions: {
		*addPostToTodo() {
			const todoPlugin = yield import( 'my-todo-plugin-module' );
			const todo = `阅读：${ state.postTitle }`.trim();
			if ( ! todoPlugin.state.todos.includes( todo ) ) {
				todoPlugin.actions.addTodo( todo );
			}
		},
	},
} );
```

## 总结

在本指南中，我们探讨了为交互性 API 存储定义类型的不同方法，从自动推断类型到手动定义类型。我们还介绍了如何处理服务器初始化状态、本地上下文和派生状态，以及如何为异步操作定义类型。

请记住，选择自动推断类型还是手动定义类型取决于您的具体需求和存储的复杂性。无论选择哪种方法，TypeScript 都将帮助您构建更好、更可靠的交互式区块。

# 使用 TypeScript

Interactivity API 为 TypeScript 提供了强大的支持，使开发者能够构建类型安全的存储库，从而通过静态类型检查、改进的代码补全和简化的重构来提升开发体验。本指南将引导您完成在 Interactivity API 存储库中使用 TypeScript 的过程，涵盖从基础类型定义到处理复杂存储结构的高级技巧。

以下是 TypeScript 与 Interactivity API 交互的核心原则：

-   **推断客户端类型**：当您使用 `store` 函数创建存储库时，TypeScript 会自动推断存储库属性（`state`、`actions` 等）的类型。这意味着您通常只需编写普通的 JavaScript 对象，TypeScript 会自动推断出类型。
-   **显式服务器类型**：当处理在服务器上定义的数据（如本地上下文或全局状态的初始值）时，您可以显式定义其类型，以确保所有内容都正确类型化。
-   **多存储部分**：即使您的存储库被拆分为多个部分，您也可以定义或推断每个部分的类型，然后将它们合并为一个代表整个存储库的类型。
-   **类型化外部存储库**：您可以从外部命名空间导入类型化的存储库，从而在类型安全的前提下使用其他插件的功能。

## 本地安装 `@wordpress/interactivity`

如果尚未安装，您需要在本地安装 `@wordpress/interactivity` 包，以便 TypeScript 可以在您的 IDE 中使用其类型。您可以使用以下命令进行安装：

`npm install @wordpress/interactivity`

保持该包的更新也是一个好习惯。

## 搭建新的类型化交互块

如果您想在本地环境中探索使用 TypeScript 的交互块示例，可以使用 `@wordpress/create-block-interactive-template`。

首先确保您的计算机上已安装 Node.js 和 `npm`。如果未安装，请查阅 [Node.js 开发环境](https://developer.wordpress.org/block-editor/getting-started/devenv/nodejs-development-environment/) 指南。

接下来，使用 [`@wordpress/create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) 包和 [`@wordpress/create-block-interactive-template`](https://www.npmjs.com/package/@wordpress/create-block-interactive-template) 模板来搭建块。

选择您要创建插件的文件夹，在该文件夹的终端中执行以下命令，并在询问时选择 `typescript` 变体。

```
npx @wordpress/create-block@latest --template @wordpress/create-block-interactive-template
```

**重要提示**：不要在终端中提供 slug。否则，`create-block` 将不会询问您要选择哪个变体，而是默认选择非 TypeScript 变体。

最后，您可以继续按照 [入门指南](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-quick-start-guide/) 中的说明操作，其余说明保持不变。

## 类型化存储库

根据存储库的结构和您的偏好，您可以选择以下三种选项之一来生成存储库的类型：

1.  从客户端存储库定义推断类型。
2.  手动类型化服务器状态，但其余部分从客户端存储库定义推断。
3.  手动编写所有类型。

### 1. 从客户端存储库定义推断类型

当您使用 `store` 函数创建存储库时，TypeScript 会自动推断存储库属性（`state`、`actions`、`callbacks` 等）的类型。这意味着您通常只需编写普通的 JavaScript 对象，TypeScript 会自动推断出正确的类型。

让我们从一个计数器块的基础示例开始。我们将在块的 `view.ts` 文件中定义存储库，其中包含初始全局状态、一个操作和一个回调。

```ts
// view.ts
const myStore = store( 'myCounterPlugin', {
	state: {
		counter: 0,
	},
	actions: {
		increment() {
			myStore.state.counter += 1;
		},
	},
	callbacks: {
		log() {
			console.log( `counter: ${ myStore.state.counter }` );
		},
	},
} );
```

如果您使用 TypeScript 检查 `myStore` 的类型，您会发现 TypeScript 已经正确推断出了类型。

```ts
const myStore: {
	state: {
		counter: number;
	};
	actions: {
		increment(): void;
	};
	callbacks: {
		log(): void;
	};
};
```

您还可以解构 `state`、`actions` 和 `callbacks` 属性，类型仍然可以正确工作。

```ts
const { state } = store( 'myCounterPlugin', {
	state: {
		counter: 0,
	},
	actions: {
		increment() {
			state.counter += 1;
		},
	},
	callbacks: {
		log() {
			console.log( `counter: ${ state.counter }` );
		},
	},
} );
```

总之，当您在单个 `store` 函数调用中定义了简单的存储库，并且不需要类型化任何在服务器上初始化的状态时，推断类型非常有用。

### 2. 手动编写服务器状态类型，其余类型从客户端存储定义推断

通过 `wp_interactivity_state` 函数在服务器端初始化的全局状态并不存在于客户端存储定义中，因此需要手动编写类型。但如果你不希望定义存储的所有类型，可以推断客户端存储定义的类型，并将其与服务器初始化状态的类型合并。

_请参阅[服务端渲染指南](/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md)了解更多关于 `wp_interactivity_state` 以及指令在服务器端如何处理的信息。_

沿用之前的示例，将 `counter` 状态的初始化移至服务器端：

```php
wp_interactivity_state( 'myCounterPlugin', array(
	'counter' => 1,
));
```

接下来，定义服务器状态类型，并将其与从客户端存储定义推断的类型合并：

```ts
// 定义服务器状态类型。
type ServerState = {
	state: {
		counter: number;
	};
};

// 将存储定义放入变量中，以便后续提取其类型。
const storeDef = {
	actions: {
		increment() {
			state.counter += 1;
		},
	},
	callbacks: {
		log() {
			console.log( `counter: ${ state.counter }` );
		},
	},
};

// 合并服务器状态和客户端存储定义的类型。
type Store = ServerState & typeof storeDef;

// 调用 `store` 函数时注入最终类型。
const { state } = store< Store >( 'myCounterPlugin', storeDef );
```

或者，如果你不介意手动编写包括服务器端和客户端定义的所有状态类型，可以强制转换 `state` 属性，并让 TypeScript 推断存储的其余部分。

假设你在客户端全局状态中有一个名为 `product` 的额外属性：

```ts
type State = {
	counter: number; // 服务器状态。
	product: number; // 客户端状态。
};

const { state } = store( 'myCounterPlugin', {
	state: {
		product: 2,
	} as State, // 手动强制转换整个状态。
	actions: {
		increment() {
			state.counter * state.product;
		},
	},
} );
```

至此，TypeScript 将从存储定义中推断 `actions` 和 `callbacks` 属性的类型，但对于 `state` 属性，它将使用 `State` 类型，以确保包含客户端和服务器定义的正确类型。

总之，这种方法适用于需要手动编写服务器状态类型，但仍希望推断存储其余部分类型的情况。

### 3. 手动编写所有类型

如果你更倾向于手动定义存储的所有类型，而不是让 TypeScript 从客户端存储定义中推断，也可以这样做。只需将这些类型传递给 `store` 函数即可。

```ts
// 定义存储类型。
interface Store {
	state: {
		counter: number; // 初始服务器状态
	};
	actions: {
		increment(): void;
	};
	callbacks: {
		log(): void;
	};
}

// 调用 `store` 函数时传递类型。
const { state } = store< Store >( 'myCounterPlugin', {
	actions: {
		increment() {
			state.counter += 1;
		},
	},
	callbacks: {
		log() {
			console.log( `counter: ${ state.counter }` );
		},
	},
} );
```

至此完成！总之，这种方法适用于你希望完全控制存储类型，并且不介意手动编写所有类型的情况。

## 为本地上下文编写类型

初始本地上下文是通过服务器端的 `data-wp-context` 指令定义的：

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

因此，你需要手动定义其类型，并将其传递给 `getContext` 函数，以确保返回的属性具有正确的类型。

```ts
// 定义上下文的类型。
type MyContext = {
	counter: number;
};

store( 'myCounterPlugin', {
	actions: {
		increment() {
			// 将其传递给 getContext 函数。
			const context = getContext< MyContext >();
			// 现在 `context` 已正确类型化。
			context.counter += 1;
		},
	},
} );
```

为了避免重复传递上下文类型，你还可以定义一个类型化函数，并使用该函数代替 `getContext`：

```ts
// 定义上下文的类型。
type MyContext = {
	counter: number;
};

// 定义一个类型化函数。只需执行一次。
const getMyContext = getContext< MyContext >;

store( 'myCounterPlugin', {
	actions: {
		increment() {
			// 使用你的类型化函数。
			const context = getMyContext();
			// 现在 `context` 已正确类型化。
			context.counter += 1;
		},
	},
} );
```

至此完成！现在你可以使用正确的类型访问上下文属性。

### 异步操作中的 yield 值类型标注

虽然 `AsyncAction<ReturnType>` 能够标注生成器函数及其最终返回值的类型，但生成器内部单个 `yield` 表达式解析的值仍可能被推断为 `any` 类型。

若需确保 `yield` 表达式解析值的类型正确（例如 `fetch` 调用或其他异步操作的结果），可使用 `TypeYield<T>` 辅助工具。该工具接收被 yield 的异步函数/操作的类型（`T`），并解析为 Promise 完成时返回值的类型。

假设 `fetchCounterData` 返回解析为对象的 Promise：

```ts
import { store, type AsyncAction, type TypeYield } from '@wordpress/interactivity';

// 假设此函数在其他地方定义，用于获取特定数据
const fetchCounterData = async ( counterValue: number ): Promise< { current: number, next: number } > => {
    // 内部逻辑...
};

const { state, actions } = store( 'myCounterPlugin', {
	state: {
		counter: 0,
	},
	actions: {
		*loadCounterData(): AsyncAction< void > {
			// 使用 TypeYield 正确标注 yield 解析值的类型
			const data = ( yield fetchCounterData( state.counter ) ) as TypeYield< typeof fetchCounterData >;

			// 此时 data 已被正确标注为 { current: number, next: number }
			console.log( data.current, data.next );

			// 根据获取的数据更新状态
			state.counter = data.next;
		},
	},
} );
```

在此示例中，`( yield fetchCounterData( state.counter ) ) as TypeYield< typeof fetchCounterData >` 确保 `data` 常量被正确标注为 `{ current: number, next: number }`，与 `fetchCounterData` 的返回类型匹配。这使您可以安全地访问 `data.current` 和 `data.next` 等属性。

## 多模块存储的类型标注

有时存储会被拆分到不同文件中。当不同区块共享相同命名空间时，每个区块会加载其所需的存储部分。

请看两个区块的示例：

-   `todo-list`：显示待办事项列表的区块
-   `add-post-to-todo`：显示按钮的区块，用于将包含“阅读 {$post_title}”文本的新待办项添加到列表

首先在服务端初始化 `todo-list` 区块的全局状态与派生状态。

```php
<?php
// todo-list-block/render.php
$todos = array( 'Buy milk', 'Walk the dog' );
wp_interactivity_state( 'myTodoPlugin', array(
  'todos'         => $todos,
  'filter'        => 'all',
  'filteredTodos' => $todos,
));
?>

<!-- HTML 标记... -->
```

现在标注服务端状态并添加客户端存储定义。注意 `filteredTodos` 是派生状态，无需手动标注类型。

```ts
// todo-list-block/view.ts
type ServerState = {
	state: {
		todos: string[];
		filter: 'all' | 'completed';
	};
};

const todoList = {
	state: {
		get filteredTodos(): string[] {
			return state.filter === 'completed'
				? state.todos.filter( ( todo ) => todo.includes( '✅' ) )
				: state.todos;
		},
	},
	actions: {
		addTodo( todo: string ) {
			state.todos.push( todo );
		},
	},
};

// 将推断类型与服务端状态类型合并
export type TodoList = ServerState & typeof todoList;

// 调用 `store` 函数时注入最终类型
const { state } = store< TodoList >( 'myTodoPlugin', todoList );
```

目前进展顺利。现在创建 `add-post-to-todo` 区块。

首先将当前文章标题添加到服务端状态。

```php
<?php
// add-post-to-todo-block/render.php
wp_interactivity_state( 'myTodoPlugin', array(
  'postTitle' => get_the_title(),
));
?>

<!-- HTML 标记... -->
```

现在标注该服务端状态并添加客户端存储定义。

```ts
// add-post-to-todo-block/view.ts
type ServerState = {
	state: {
		postTitle: string;
	};
};

const addPostToTodo = {
	actions: {
		addPostToTodo() {
			const todo = `阅读：${ state.postTitle }`.trim();
			if ( ! state.todos.includes( todo ) ) {
				actions.addTodo( todo );
			}
		},
	},
};

// 将推断类型与服务端状态类型合并
type Store = ServerState & typeof addPostToTodo;

// 调用 `store` 函数时注入最终类型
const { state, actions } = store< Store >( 'myTodoPlugin', addPostToTodo );
```

这在浏览器中运行正常，但 TypeScript 会提示此区块中缺少 `state.todos` 和 `actions.addtodo`。

要解决此问题，需要从 `todo-list` 区块导入 `TodoList` 类型并与其他类型合并。

```ts
import type { TodoList } from '../todo-list-block/view';

// ...

// 将推断类型与服务端状态类型合并
type Store = TodoList & ServerState & typeof addPostToTodo;
```

现在 TypeScript 将识别 `state.todos` 和 `actions.addTodo` 在 `add-post-to-todo` 区块中的可用性。

这种方法使 `add-post-to-todo` 区块能与现有待办列表交互，同时保持类型安全并为共享存储添加自身功能。

若需在 `todo-list` 区块中使用 `add-post-to-todo` 的类型，只需导出其类型并导入到另一个 `view.ts` 文件中。

最后，如果希望手动定义所有类型而非推断，可在单独文件中定义类型，并将该定义导入到每个存储部分。以下是待办列表示例的实现方式：

```ts
// types.ts
interface Store {
	state: {
		todos: string[];
		filter: 'all' | 'completed';
		filtered: string[];
		postTitle: string;
	};
	actions: {
		addTodo( todo: string ): void;
		addPostToTodo(): void;
	};
}

export default Store;
```

```ts
// todo-list-block/view.ts
import type Store from '../types';

const { state } = store< Store >( 'myTodoPlugin', {
	// 此处所有内容均正确标注类型
} );
```

```ts
// add-post-to-todo-block/view.ts
import type Store from '../types';

const { state, actions } = store< Store >( 'myTodoPlugin', {
	// 此处所有内容均正确标注类型
} );
```

这种方法使您能完全掌控类型，并确保存储所有部分的一致性。当存储结构复杂或需要在多个区块/组件中强制实施特定接口时，此方法尤其有用。

## 派生状态的类型定义

派生状态是基于全局状态或局部上下文计算得出的数据。在客户端存储定义中，它通过 `state` 对象中的 getter 来定义。

_请访问[理解全局状态、局部上下文和派生状态](/docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md)指南，了解更多关于交互性 API 中派生状态的工作原理。_

延续之前的示例，我们创建一个派生状态，使其值为计数器值的两倍。

```ts
type MyContext = {
	counter: number;
};

const myStore = store( 'myCounterPlugin', {
	state: {
		get double() {
			const { counter } = getContext< MyContext >();
			return counter * 2;
		},
	},
	actions: {
		increment() {
			state.counter += 1; // 此处的类型为 number。
		},
	},
} );
```

通常情况下，当派生状态依赖于局部上下文时，TypeScript 能够推断出正确的类型：

```ts
const myStore: {
	state: {
		readonly double: number;
	};
	actions: {
		increment(): void;
	};
};
```

但当派生状态的返回值直接依赖于全局状态的某一部分时，TypeScript 将无法推断类型，因为它会声称存在循环引用。

例如，在这种情况下，TypeScript 无法推断 `state.double` 的类型，因为它依赖于 `state.counter`，而 `state` 的类型在 `state.double` 的类型定义完成之前无法确定，从而形成了循环引用。

```ts
const { state } = store( 'myCounterPlugin', {
	state: {
		counter: 0,
		get double() {
			// TypeScript 无法推断此返回类型，因为它依赖于 `state`。
			return state.counter * 2;
		},
	},
	actions: {
		increment() {
			state.counter += 1; // 此处的类型现在为 unknown。
		},
	},
} );
```

在这种情况下，根据你的 TypeScript 配置，TypeScript 可能会警告你存在循环引用，或者直接将 `any` 类型赋给 `state` 属性。

然而，解决这个问题很简单；我们只需要手动为 TypeScript 提供该 getter 的返回类型。一旦我们这样做，循环引用就会消失，TypeScript 将能够再次推断所有 `state` 类型。

```ts
const { state } = store( 'myCounterPlugin', {
	state: {
		counter: 1,
		get double(): number {
			return state.counter * 2;
		},
	},
	actions: {
		increment() {
			state.counter += 1; // 正确推断！
		},
	},
} );
```

现在，以下是之前存储的正确推断类型。

```ts
const myStore: {
	state: {
		counter: number;
		readonly double: number;
	};
	actions: {
		increment(): void;
	};
};
```

在服务器端使用 `wp_interactivity_state` 时，请记住你还需要定义派生状态的初始值，如下所示：

```php
wp_interactivity_state( 'myCounterPlugin', array(
	'counter' => 1,
	'double'  => 2,
));
```

但如果你正在推断类型，则无需手动定义派生状态的类型，因为它已经存在于你的客户端存储定义中。

```ts
// 你不需要在此处定义 `state.double` 的类型。
type ServerState = {
	state: {
		counter: number;
	};
};

// `state.double` 的类型从此处推断。
const storeDef = {
	state: {
		get double(): number {
			return state.counter * 2;
		},
	},
	actions: {
		increment() {
			state.counter += 1;
		},
	},
};

// 合并服务器状态和客户端存储定义的类型。
type Store = ServerState & typeof storeDef;

// 在调用 `store` 函数时注入最终类型。
const { state } = store< Store >( 'myCounterPlugin', storeDef );
```

就是这样！现在你可以使用正确的类型访问派生状态属性。

## 异步操作的类型定义

在使用 TypeScript 与交互性 API 时，另一件需要注意的事情是，异步操作必须使用生成器而不是异步函数来定义。

在交互性 API 的异步操作中使用生成器的原因是，能够在异步操作在 yield 后继续执行时，恢复最初触发操作的作用域。但这只是语法上的改变，**除此之外，这些函数的操作方式与常规异步函数完全相同**，并且 `store` 函数推断出的类型反映了这一点。

延续之前的示例，我们向存储添加一个异步操作。

```ts
const { state } = store( 'myCounterPlugin', {
	state: {
		counter: 0,
		get double(): number {
			return state.counter * 2;
		},
	},
	actions: {
		increment() {
			state.counter += 1;
		},
		*delayedIncrement() {
			yield new Promise( ( r ) => setTimeout( r, 1000 ) );
			state.counter += 1;
		},
	},
} );
```

此存储的推断类型为：

```ts
const myStore: {
	state: {
		counter: number;
		readonly double: number;
	};
	actions: {
		increment(): void;
		// 此处的行为类似于常规异步函数。
		delayedIncrement(): Promise< void >;
	};
};
```

这也意味着你可以在外部函数中使用异步操作，并且 TypeScript 将正确使用异步函数类型。

```ts
const someAsyncFunction = async () => {
	// 这可以正常工作，并且类型正确。
	await actions.delayedIncrement( 2000 );
};
```

当你不推断类型而是手动为整个存储编写类型时，可以为异步操作使用异步函数类型。

```ts
type Store = {
	state: {
		counter: number;
		readonly double: number;
	};
	actions: {
		increment(): void;
		delayedIncrement(): Promise< void >; // 你可以在此处使用异步函数。
	};
};
```

在使用异步操作时，需要注意一点。与派生状态类似，如果异步操作在 `yield` 表达式中使用 `state`（例如，将 `state` 传递给一个随后被 yield 的异步函数），或者其返回值依赖于 `state`，TypeScript 可能由于潜在的循环引用而无法正确推断类型。

```ts
const { state, actions } = store( 'myCounterPlugin', {
	state: {
		counter: 0,
	},
	actions: {
		*delayedOperation() {
			// 示例：state.counter 被用作 yield 逻辑的一部分。
			yield fetchCounterData( state.counter );

			// 并且/或者最终返回值依赖于 state。
			return state.counter + 1;
		},
	},
} );
```

在这种情况下，TypeScript 可能会发出关于循环引用的警告，或者默认使用 `any` 类型。为了解决这个问题，你需要手动为生成器函数定义类型。交互性 API 为此提供了一个辅助类型 `AsyncAction<ReturnType>`。

```ts
import { store, type AsyncAction } from '@wordpress/interactivity';

const { state, actions } = store( 'myCounterPlugin', {
	state: {
		counter: 0,
	},
	actions: {
		*delayedOperation(): AsyncAction< number > {
			// 现在，这不会导致循环引用。
			yield fetchCounterData( state.counter );

			// 现在，类型正确。
			return state.counter + 1;
		},
	},
} );
```

就是这样！`AsyncAction<ReturnType>` 辅助类型被定义为 `Generator<any, ReturnType, unknown>`。通过使用 `any` 作为生成器 yield 的值的类型，它有助于打破循环引用，使得 TypeScript 能够在 `state` 参与 `yield` 表达式或最终返回值时正确推断类型。你只需要指定异步操作的最终 `ReturnType` 即可。