Добавление TypeScript в @wordpress/data Stores
Опубликовано: 2021-02-05В прошлом году мы много говорили о TypeScript. В одном из моих последних постов мы увидели, как использовать TypeScript в ваших плагинах WordPress на реальном примере и, в частности, как улучшить магазин 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 и столкнулся с проблемой при применении всех этих методов. Итак, давайте рассмотрим указанную проблему и узнаем, как ее преодолеть!
Эта проблема
Как вы, возможно, уже знаете, когда мы хотим использовать селекторы и/или действия, которые мы определили в нашем магазине, мы делаем это, обращаясь к ним через хуки 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, мы можем указать, что когда мы вызываем методы select или dispatch пакета @wordpress/data с именем нашего хранилища в качестве параметра, результатом, который мы получаем, являются наши селекторы и наши действия соответственно.
Для этого просто добавьте блок 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(тип когда условие ложно)». -
F extends XXX. Это формула для определения условия. Вы хотите проверить, чтоFявляется строкой? Просто введите:F extends string. Очень просто. А как насчет «функции с одним аргументом»? Это, конечно, звучит сложнее… -
(x: any, ...args: infer P) => infer R. Это тип функции: мы начинаем с аргументов (в скобках), за которыми следует стрелка, за которой следует тип возвращаемого значения функции. В этом конкретном случае мы требуем, чтобы функция имела один аргументx(чей конкретный тип не имеет значения). В этом определении типа есть два интересных момента. С одной стороны, мы используем оператор rest для захвата типовPоставшихсяargs(если они есть). С другой стороны, мы используем вывод типа 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; Легко, верно? Это очень похоже на то, что мы делали в предыдущем разделе: мы фиксируем типы args в P (на этот раз нет необходимости требовать хотя бы один аргумент 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; }Вот и все! Надеюсь, вам понравился этот совет от разработчиков, и если да, поделитесь им со своими коллегами и друзьями. Ой! А если вы знаете другой подход для получения того же результата, расскажите в комментариях.
Избранное изображение Габриэля Крисмариу на Unsplash.
