将 TypeScript 添加到 @wordpress/data 存储

已发表: 2021-02-05

去年我们谈了很多关于 TypeScript 的事情。 在我最近的一篇文章中,我们通过一个真实示例了解了如何在您的 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 ) { … }

唯一能让我们了解每个函数的作用和每个参数是什么的线索取决于我们的命名能力,以下改进的 TypeScript 对应物:

 // 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,在应用所有这些技术时遇到了问题。 因此,让我们回顾一下上述问题并学习如何克服它!

问题

您可能已经知道,当我们想要使用我们在 store 中定义的选择器和/或操作时,我们通过 React 钩子(使用useSelectuseDispatch )或通过高阶组件(使用withSelectwithDispatch )访问它们来实现,所有这些都由@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 是如何知道这些选择器和操作存在的,更不用说它的类型是什么了?

嗯,这正是我面临的问题。 也就是说,我想知道如何告诉 TypeScript 访问select('nelio-store')的结果是一个包含我们所有商店选择器的对象,而dispatch('nelio-store')是一个包含我们商店操作的对象.

解决方案

在我们上一篇关于 TypeScript 的文章中,我们谈到了多态函数。 多态函数让我们可以根据给定的参数指定不同的返回类型。 好吧,使用 TypeScript 多态性,我们可以指定,当我们以我们的 store 名称作为参数调用@wordpress/data包的selectdispatch方法时,我们得到的结果分别是我们的选择器和我们的操作。

为此,只需在我们注册商店的文件中添加一个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; }

然后定义SelectorsActions类型实际上是什么:

 type Selectors = { getPost: ( id: PostId ) => Post | undefined; getPostsInDay: ( day: Day ) => PostId[]; } type Actions = { receiveNewPost: ( post: Post ) => void; updatePost: ( postId: PostId, attributes: Partial<Post> ) => void; }

到目前为止,一切都很好,对吧? 唯一的“问题”是我们必须手动定义SelectorsActions类型,这听起来很奇怪,因为 TypeScript 已经知道我们有一组正确类型的selectorsactions ......

在 TypeScript 中操作函数类型

如果我们看一下我们导入的actionsselectors对象的类型,我们会看到 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调用的操作不返回任何内容)。

我们可以使用它们来自动生成我们需要的SelectorsActions类型吗?

如何在 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>这样的东西是可能的……尽管我们知道这种类型应该只与至少有一个参数的函数一起使用。 为了确保这个辅助类型只用于函数,我们将它定义为条件类型。 条件类型让我们根据条件指定结果类型应该是什么:“如果F是具有至少一个参数(条件)的函数,则结果类型是另一个函数,其中第一个参数已被删除(条件为真的); 否则,使用never类型(条件为 false 时的类型)。”
  • F extends XXX 。 这是指定条件的公式。 你想检查F是一个字符串吗? 只需输入: F extends string 。 十分简单。 但是“只有一个参数的函数”呢? 这听起来确实更复杂……
  • (x: any, ...args: infer P) => infer R 。 这是一个函数类型:我们从参数(括号中)开始,然后是一个箭头,然后是函数的返回类型。 在这种特殊情况下,我们要求函数有一个参数x (其具体类型无关紧要)。 这个类型定义有两个有趣的部分。 一方面,我们使用 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;

容易,对吧? 这与我们在上一节中所做的非常相似:我们捕获Pargs的类型(这次不需要至少一个参数x )并忽略返回类型。 如果F是一个函数,则返回一个返回void的新函数。 否则, never返回。 惊人的!

在操场上看看这个。

如何在 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; }

在前两节中,我们学习了如何将一种类型的函数转换为另一种类型的函数。 这意味着我们可以像这样手动定义新类型:

 type Selectors = { getPost: OmitFirstArg< typeof selectors.getPost >; getPostsInDay: OmitFirstArg< typeof selectors.getPostsInDay >; }; type Actions = { receiveNewPost: RemoveReturnType< actions.receiveNewPost >; updatePost: RemoveReturnType< actions.updatePost >; };

但是,随着时间的推移,这当然是不可持续的:我们手动指定了两种类型的函数名称。 显然,我们希望将actionsselectors的原始类型定义自动映射到新类型。

以下是在 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 的内容:我们想要所有的键K都是keyof O的键。
  • 然后,对于每个键K ,它的类型是OmitFirstArg<O[K]> 。 也就是说,我们得到原始类型( O[K] ),然后使用我们定义的辅助类型(在本例中为OmitFirstArg )将其转换为我们想要的类型。
  • 最后,我们对RemoveReturnTypes和原始辅助类型RemoveReturnType执行相同的操作。

使用我们的选择器和操作扩展@wordpress/data

如果将我们今天看到的四种辅助类型添加到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; }

就是这样! 我希望你喜欢这个开发技巧,如果你喜欢,请与你的同事和朋友分享。 哦! 如果您知道获得相同结果的不同方法,请在评论中告诉我。

Gabriel Crismariu 在 Unsplash 上的特色图片。