將 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 鉤子(使用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 是如何知道這些選擇器和操作存在的,更不用說它的類型是什麼了?
嗯,這正是我面臨的問題。 也就是說,我想知道如何告訴 TypeScript 訪問select('nelio-store')的結果是一個包含我們所有商店選擇器的對象,而dispatch('nelio-store')是一個包含我們商店操作的對象.
解決方案
在我們上一篇關於 TypeScript 的文章中,我們談到了多態函數。 多態函數讓我們可以根據給定的參數指定不同的返回類型。 好吧,使用 TypeScript 多態性,我們可以指定,當我們以我們的 store 名稱作為參數調用@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>這樣的東西是可能的……儘管我們知道這種類型應該只與至少有一個參數的函數一起使用。 為了確保這個輔助類型只用於函數,我們將它定義為條件類型。 條件類型讓我們根據條件指定結果類型應該是什麼:“如果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; 容易,對吧? 這與我們在上一節中所做的非常相似:我們捕獲P中args的類型(這次不需要至少一個參數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 >; }; 但是,隨著時間的推移,這當然是不可持續的:我們手動指定了兩種類型的函數名稱。 顯然,我們希望將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 的內容:我們想要所有的鍵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 上的特色圖片。
