Первоначально вопрос состоял из двух вопросов, «объединенных» вместе, но после обсуждения в комментариях и столь необходимой помощи @jcalz нам удалось приблизить его к тому, как он выглядит сейчас.
Вы можете найти полные примеры кода в конце для более удобного копирования и вставки.
Вы можете найти определения типов и примеры кода ниже.
Я пытаюсь найти способ «составить» (как в композиции функций) несколько функций, которые должны изменить один объект, «расширив» его дополнительными свойствами, в единую функцию, которая выполняет все расширения и является правильно напечатано.
Речь идет о функциях StoreEnhancers<Ext>
(где Ext
представляет собой простой объект, которым расширяется результирующий объект), и результатом их композиции также должен быть StoreEnhancer<ExtFinal>
, где ExtFinal
должен быть объединением всех Ext
каждого энхансера, который был передан в состав.
Независимо от того, что я пытаюсь передать массив с помощью оператора распространения (...
), я не могу написать функцию компоновки, которая способна расширить объект с помощью нескольких StoreEnhancers
и позволить машинописному тексту вывести окончательный Ext
, чтобы я мог получить доступ все свойства, которые добавляют эти усилители.
Вот несколько определений для большего контекста:
Во-первых, мы можем определить StoreEnhancer
как функцию, которая принимает либо StoreCreator
, либо EnahncedStoreCreator
и возвращает EnhancedStoreCreator
. Или, говоря более понятным языком, это функция, которая принимает в качестве аргумента другую функцию, используемую для создания того, что мы назовем «объектом хранилища». Улучшитель хранилища затем «улучшит» этот объект хранилища, добавив к нему дополнительные свойства, и вернет «расширенную» версию объекта хранилища.
Итак, давайте определим типы (очень простые, для простоты)
type Store = {
tag: 'basestore' // used just to make Store distinct from a base {}
}
type StoreCreator = () => Store
type EnhancedStoreCreator<Ext> = () => Store & Ext
// essentially one could say that:
// StoreCreator === EnhancedStoreCreator<{}>
// which allows us to define a StoreEnhancer as such:
type StoreEnhancer<Ext> = <Prev>(createStore: EnhancedStoreCreator<Prev>) => EnhancedStoreCreator<Ext & Prev>
И реализация может выглядеть примерно так:
const createStore: StoreCreator = () => ({ tag: 'basestore' })
const enhanceWithFeatureA: StoreEnhancer<{ featureA: string }> = createStore => () => {
const prevStore = createStore()
return { ...prevStore, featureA: 'some string' }
}
const enhanceWithFeatureB: StoreEnhancer<{ featureB: number }> = createStore => () => {
const prevStore = createStore()
return { ...prevStore, featureB: 123 }
}
const createStoreWithA = enhanceWithFeatureA(createStore)
const createStoreWithAandB = enhanceWithFeatureB(createStoreWithA)
const store = storeCreatorWithFeatureAandB()
console.info(store)
// {
// tag: 'baseStore',
// featureA: 'some string'
// featureB: 123
// }
Ссылка Codesandbox с новым (обновленным) кодом здесь
Ссылка Codesandbox с исходным кодом вопроса здесь
Конечно, вы хотели бы использовать внешний код, например, codeandbox? @jcalz
Это должен быть открытый текст в самом вопросе, хотя внешняя ссылка является хорошим дополнением. Вкрапление текста допустимо, если в нем нет опечаток или несвязанных ошибок.
Кстати, я думаю, что этот подход может удовлетворить ваши потребности, но мне пришлось исправить несколько опечаток, чтобы добраться туда. Если это сработает для вас, я мог бы написать ответ, но я был бы признателен, если бы вы могли отредактировать код вопроса, чтобы исправить те же проблемы.
Я добавил обе ссылки в сообщение, я также изменил некоторый код, чтобы не было шрифтов, но, возможно, мне не удалось отследить их все, когда я писал вопрос, я написал его в своем редакторе, но я написал это «быстро и грязно», и при вставке сюда я изменил имена, чтобы они были более понятными, поэтому некоторые из них все еще могут просвечиваться, так или иначе, на codeandbox вы можете найти рабочий пример, одним щелчком мыши
Подождите, вы говорите мне, что сфера действия дженерика — это ВСЕ, что было? Я в шоке.... Но все равно остается вопрос, как мне теперь использовать массив этих a как композицию? Не могли бы вы помочь с этим?
Удалось заставить композицию работать? @jcalz С вашим предложенным обзором универсального (кстати, спасибо за это, я даже не знал, что это когда-либо может иметь значение, я читал официальные документы TS (хотя и некоторое время назад), но никогда не нашел упоминания об охвате дженерики и как они повлияют на код.) В любом случае, с областью видимости я могу заставить работать 2 энхансера, 3 энхансера, х энхансеров... пока я пишу их вручную... Любая попытка использовать массив повторить не удалось. Кажется, я не могу заставить TS фактически взять X количество энхансеров и получить результирующий тип.
Ах, правильно, вопрос сформулирован так, что он в первую очередь касается состава этих вещей, хотя у вас они не работали по отдельности. Что вы хотите сделать, отредактировать вопрос, чтобы отдельные вещи были в порядке, а затем просто сосредоточиться на композиции? Или разделить на два вопроса, чтобы этот вопрос касался того, чтобы отдельные «усилители» работали в целом, а второй — о композиции? Композиционное решение, вероятно, будет выглядеть вот так.
Я думаю, что было бы более разумно отредактировать вопрос, чтобы он был правильно напечатан в соответствии с вашим предложением, а затем сделать более очевидным, что речь идет о композиции, и в этот момент я хотел бы, чтобы вы опубликовали ответ. Кажется, это нормально? Кроме того, где в мире вы узнали TS, как это, эта штука TupleToIntersection, что это за колдовство, а также определение "... args" как объекта, чьи ключи являются ключами массива, о.0, почему это даже работает, или, что еще хуже, как это вообще удается правильно извлекать T. Если у вас есть терпение, когда вы пишете свой ответ, я хотел бы, чтобы вы объяснили это.
Я могу кое-что объяснить, но в основном я буду давать ссылки на документацию, когда речь идет о таких вещах, как сопоставленные типы массивов/кортежей или вывод из сопоставленных типов.
Редактируя вопрос и ожидая любой информации, которую вы можете предоставить, я ломал голову над этим в течение последних 3 недель, 8-часовой рабочий день ... Я понимаю, что не силен в TS, но это какой-то следующий уровень взлом, редактирую сейчас, скоро будет
Хорошо, я сделаю это, когда у меня будет шанс; это может быть не в течение нескольких часов.
Перемещение по общему правилу, предложенное @jcalz, должно решить эти проблемы. Каков фактический вариант использования? А что не работает со встроенным redux типа StoreEnhancer? По общему признанию, я никогда не пишу энхансеры, поэтому у меня нет опыта использования этого типа. Меня смущает, что и next
, и возвращаемый тип являются расширенной версией, но я не полностью окунулся в нее.
На самом деле я получил кучу ошибок, когда копировал и вставлял тестовый код redux Enhancer в TS Playground, так что вы можете что-то здесь понять.
Похоже, есть открытый PR для исправления типов в Redux и две связанные проблемы
Существует множество проблем с исходной версией Redux, как она есть, с ней ужасно сложно работать, она лжет о том, что она на самом деле делает, она не описывает, что на самом деле делает код js в некоторых случаях. И это только в том случае, если вы хотите использовать его как есть. Я хочу расширить его, что делает все это моим худшим кошмаром... Но мне это нужно, чтобы создать безопасную для типов среду для полностью развязанной модульной структуры (какой-то)...
@jcalz Я обновил вопрос и с нетерпением жду вашего ответа с максимально возможными объяснениями / подробностями. Код, который вы мне дали, работает, но, на мой взгляд, это выглядит как чрезвычайно хакерский способ передачи информации компилятору TS, информацию, которую он должен иметь возможность получить другими способами, но, по-видимому, это работает, поэтому я больше не знаю.
Ну вот. Некоторые из них представляют собой несколько продвинутое жонглирование типа TS, но я бы сказал, что «хакерство» в глазах смотрящего ... все здесь использует поддерживаемые функции, хотя TupleToIntersection<T>
немного ближе к краю, чем все остальное.
Привет, народ. Из любопытства, какой фактический вариант использования вызвал этот вопрос в первую очередь? Почему вы хотите написать свой собственный метод composeEnhancers
? Какую проблему ты пытаешься решить? @dellirium, можете ли вы уточнить, что вы подразумеваете под «оригинальным compose
Redux ложью, и с ним трудно работать»? Я не слышал, чтобы кто-то говорил что-то подобное раньше.
Все это является лишь частью «улучшений», которые я делаю при переупаковке избыточности в более «отдельную» версию в комплекте. Идея/пункт состоит в том, чтобы позволить нескольким меньшим "связкам" писаться отдельно и иметь их "предварительно скомпилированные" (из-за отсутствия лучшего слова) в единое хранилище, которое вы можете использовать внутри приложения. Я уже сделал это на простом JavaScript, но очевидно, что с TS есть дополнительное преимущество, заключающееся в возможности IntelliSense структуры, и, таким образом, это делает DevExp лучше. Продолжение в следующем комментарии:
Проблема с использованием обычного компоновщика заключается в том, что он просто не может передавать информацию о типе. Проблема со всеми остальными частями Redux в том, что они неверны или неполны. Проблема энхансеров магазина с PR Линдой, упомянутая выше, — это только вершина айсберга, реализация createStore не соответствует типам, это просто огромная головная боль. Даже не заводите меня о промежуточном программном обеспечении... Предполагается, что промежуточное программное обеспечение может перехватывать действия, но вы не можете сделать свои действия чем-либо, что не расширяет базовое действие.
Цель состоит в том, чтобы написать функцию composeEnhancers()
, которая принимает переменное количество аргументов, каждый из которых является значением StoreEnhancer<TI>
для некоторого TI
; то есть аргументы будут иметь тип кортежа [StoreEnhancer<T0>, StoreEnhancer<T1>, StoreEnhancer<T2>,
... , StoreEnhancer<TN>]
) для некоторых типов от T0
до TN
. И он должен возвращать значение типа StoreEnhancer<R>
, где R
— это пересечение всех TI
типов; то есть StoreEnhancer<T0 & T1 & T2 &
... & TN>
.
Прежде чем мы реализуем функцию, давайте разработаем ее типизацию, написав сигнатуру вызова. Из приведенного выше описания кажется, что мы имеем дело с базовым типом кортежа [T0, T1, T2,
... , TN]
, который сопоставляется, чтобы стать типом ввода. Назовем тип кортежа T
и скажем, что в каждом числовом индексе I
элемент T[I]
сопоставляется с StoreEnhancer<T[I]>
. К счастью, эту операцию очень просто представить с помощью сопоставленного типа , потому что сопоставленные типы, которые работают с массивами/кортежами, также создают массивы/кортежи.
Итак, на данный момент у нас есть
declare function composeEnhancers<T extends any[]>(
...enhancers: { [I in keyof T]: StoreEnhancer<T[I]> }
): StoreEnhancer<???>;
где оставшийся параметр относится к соответствующему типу сопоставленного кортежа. Обратите внимание, что этот сопоставленный тип является гомоморфным (см. Что означает «гомоморфный сопоставленный тип»? ), и поэтому компилятор может довольно легко вывести T
из значения сопоставленного типа (такое поведение называется «вывод из сопоставленных типов» и раньше это было задокументировано здесь, но в новой версии справочника, похоже, об этом не упоминается). Поэтому, если вы вызываете composeEnhancers(x, y, z)
, где x
имеет тип StoreEnhancer<X>
, y
имеет тип StoreEnhancer<Y>
, а z
имеет тип StoreEnhancer<Z>
, то компилятор легко сделает вывод, что T
— это [X, Y, Z]
.
Хорошо, а как насчет возвращаемого типа? Нам нужно заменить ???
типом, представляющим пересечение всех элементов T
. Давайте просто дадим этому имя:
declare function composeEnhancers<T extends any[]>(
...enhancers: { [I in keyof T]: StoreEnhancer<T[I]> }
): StoreEnhancer<TupleToIntersection<T>>;
А теперь нам нужно определить TupleToIntersection
. Ну, вот одна из возможных реализаций:
type TupleToIntersection<T extends any[]> =
{ [I in keyof T]: (x: T[I]) => void }[number] extends
(x: infer U) => void ? U : never;
При этом используется функция, в которой вывод типов в условных типах создает пересечение типов-кандидатов, если сайты вывода находятся в контравариантном положении (см. Разница между дисперсией, ковариантностью, контравариантностью и бивариантностью в TypeScript ), например, функцией параметр. Поэтому я сопоставил T
с версией, где каждый элемент является параметром функции, объединил их в единый союз функций, а затем сделал вывод о единственном типе параметра функции, который становится пересечением. Это техника, аналогичная тому, что показано в Преобразование типа объединения в тип пересечения.
Хорошо, теперь у нас есть позывной. Давайте убедимся, что вызывающая сторона видит желаемое поведение:
const enhanceWithFeatureA: StoreEnhancer<{ featureA: string }> =
cs => () => ({ ...cs(), featureA: 'some string' });
const enhanceWithFeatureB: StoreEnhancer<{ featureB: number }> =
cs => () => ({ ...cs(), featureB: 123 });
const enhanceWithFeatureC: StoreEnhancer<{ featureC: boolean }> =
cs => () => ({ ...cs(), featureC: false });
const enhanceWithABC = composeEnhancers(
enhanceWithFeatureA, enhanceWithFeatureB, enhanceWithFeatureC
);
/* const enhanceWithABC: StoreEnhancer<{
featureA: string;
} & {
featureB: number;
} & {
featureC: boolean;
}> */
Выглядит неплохо; значение enhanceWithABC
представляет собой одиночное StoreEnhancer
, аргумент типа которого является пересечением аргументов типа входных StoreEnhancer
s.
И мы, по сути, закончили. Функцию все еще нужно реализовать, и реализация достаточно проста, но, к сожалению, поскольку сигнатура вызова довольно сложна, нет надежды, что компилятор сможет проверить, действительно ли реализация полностью соответствует сигнатуре вызова:
function composeEnhancers<T extends any[]>(
...enhancers: { [I in keyof T]: StoreEnhancer<T[I]> }
): StoreEnhancer<TupleToIntersection<T>> {
return creator => enhancers.reduce((acc, e) => e(acc), creator); // error!
// Type 'EnhancedStoreCreator<Prev>' is not assignable to type
// 'EnhancedStoreCreator<TupleToIntersection<T> & Prev>'.
}
Это будет работать во время выполнения, но компилятор понятия не имеет, что метод уменьшения() массива выведет значение правильного типа. Он знает, что вы получите EnhancedStoreCreator
, но не конкретно связанный с TupleToIntersection<T>
. По сути, это ограничение языка TypeScript; типизация для reduce()
не может быть сделана достаточно общей, чтобы даже выразить вид постепенного изменения типа от начала до конца основного цикла; см. Ввод сокращения над кортежем Typescript .
Так что лучше не пробовать. Мы должны стремиться подавить ошибку и просто убедиться, что наша реализация написана правильно (потому что компилятор не может сделать это за нас).
Один из способов продолжить — убрать «отключить проверку типов» любого типа там, где есть проблемные места:
function composeEnhancers<T extends any[]>(
...enhancers: { [I in keyof T]: StoreEnhancer<T[I]> }
): StoreEnhancer<TupleToIntersection<T>> {
return (creator: EnhancedStoreCreator<any>) =>
// ------------------------> ^^^^^
enhancers.reduce((acc, e) => e(acc), creator); // okay
}
Теперь ошибки нет, и это почти лучшее, что мы можем здесь сделать. Существуют и другие подходы к подавлению ошибки, такие как утверждения типа или перегрузки одиночной подписи вызова, но я не буду отвлекаться на подробное изучение их здесь.
И теперь, когда у нас есть типизация и реализация, давайте просто убедимся, что наша функция enhanceWithABC()
работает как положено:
const allThree = enhanceWithABC(createStore)();
/* const allThree: Store & {
featureA: string;
} & {
featureB: number;
} & {
featureC: boolean;
} & {
readonly tag: "basestore";
} */
console.info(allThree);
/* {
"tag": "basestore",
"featureA": "123",
"featureB": 123,
"featureC": false
} */
console.info(allThree.featureB.toFixed(2)) // "123.00"
Выглядит неплохо!
Вы буквально спаситель жизни. Я все еще очень сбит с толку всей этой контрвариантной хакерской штуковиной, но теперь это работает, и у меня есть проблемы другого характера, с которыми мне приходится иметь дело сейчас. Я могу просто надеяться, что вы или кто-то с таким же знанием, как и вы, увидите мои следующие несколько сообщений здесь. Я ломал голову по 8 с лишним часов в день на протяжении более трех недель… Я разговаривал со всеми разработчиками, которых знал, спрашивал в различных группах Discord, долго разговаривал с chatGPT, но безрезультатно. Я хотел бы спросить еще одну вещь (но не хватает места для этого комментария)
«Трюк», используемый для отображения кортежа, немного «обратный» в моей голове, вы сказали, что тип T — это кортеж [X, Y, Z], который отображается на входы, что является самым встречным -интуитивный способ взглянуть на это с моей точки зрения, но я понимаю, как это работает. Можно ли использовать тот же трюк для «извлечения» информации о типах возвращаемых значений из массива функций, возвращающих простые объекты, а затем, конечно же, смешать эти объекты вместе... и пока я пишу это, я понимаю, да, это было бы... Чувак, это какой-то следующий уровень $h!7. Спасибо за объяснение, очень признателен.
Возможно, но без минимального воспроизводимого примера точно говорить не хотелось бы. В любом случае мне нужно расписаться на ночь.
Я хотел бы добавить код в свою IDE и начать над ним работать; к сожалению, кажется, что он перемежается большим количеством текста и содержит по крайней мере одну опечатку, поэтому я предполагаю, что он никогда не проходил через IDE, чтобы попасть сюда. Не могли бы вы предоставить простой в использовании минимальный воспроизводимый пример, чтобы я мог добраться до стартовой линии без лишней дополнительной работы?