Вывод аргумента типа из цепочки в составе функций [compose]

Первоначально вопрос состоял из двух вопросов, «объединенных» вместе, но после обсуждения в комментариях и столь необходимой помощи @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 с исходным кодом вопроса здесь

Я хотел бы добавить код в свою IDE и начать над ним работать; к сожалению, кажется, что он перемежается большим количеством текста и содержит по крайней мере одну опечатку, поэтому я предполагаю, что он никогда не проходил через IDE, чтобы попасть сюда. Не могли бы вы предоставить простой в использовании минимальный воспроизводимый пример, чтобы я мог добраться до стартовой линии без лишней дополнительной работы?

jcalz 02.02.2023 19:55

Конечно, вы хотели бы использовать внешний код, например, codeandbox? @jcalz

Dellirium 02.02.2023 19:56

Это должен быть открытый текст в самом вопросе, хотя внешняя ссылка является хорошим дополнением. Вкрапление текста допустимо, если в нем нет опечаток или несвязанных ошибок.

jcalz 02.02.2023 19:57

Я добавил обе ссылки в сообщение, я также изменил некоторый код, чтобы не было шрифтов, но, возможно, мне не удалось отследить их все, когда я писал вопрос, я написал его в своем редакторе, но я написал это «быстро и грязно», и при вставке сюда я изменил имена, чтобы они были более понятными, поэтому некоторые из них все еще могут просвечиваться, так или иначе, на codeandbox вы можете найти рабочий пример, одним щелчком мыши

Dellirium 02.02.2023 20:19

Подождите, вы говорите мне, что сфера действия дженерика — это ВСЕ, что было? Я в шоке.... Но все равно остается вопрос, как мне теперь использовать массив этих a как композицию? Не могли бы вы помочь с этим?

Dellirium 02.02.2023 20:27

Удалось заставить композицию работать? @jcalz С вашим предложенным обзором универсального (кстати, спасибо за это, я даже не знал, что это когда-либо может иметь значение, я читал официальные документы TS (хотя и некоторое время назад), но никогда не нашел упоминания об охвате дженерики и как они повлияют на код.) В любом случае, с областью видимости я могу заставить работать 2 энхансера, 3 энхансера, х энхансеров... пока я пишу их вручную... Любая попытка использовать массив повторить не удалось. Кажется, я не могу заставить TS фактически взять X количество энхансеров и получить результирующий тип.

Dellirium 02.02.2023 21:13

Я думаю, что было бы более разумно отредактировать вопрос, чтобы он был правильно напечатан в соответствии с вашим предложением, а затем сделать более очевидным, что речь идет о композиции, и в этот момент я хотел бы, чтобы вы опубликовали ответ. Кажется, это нормально? Кроме того, где в мире вы узнали TS, как это, эта штука TupleToIntersection, что это за колдовство, а также определение "... args" как объекта, чьи ключи являются ключами массива, о.0, почему это даже работает, или, что еще хуже, как это вообще удается правильно извлекать T. Если у вас есть терпение, когда вы пишете свой ответ, я хотел бы, чтобы вы объяснили это.

Dellirium 02.02.2023 22:00

Я могу кое-что объяснить, но в основном я буду давать ссылки на документацию, когда речь идет о таких вещах, как сопоставленные типы массивов/кортежей или вывод из сопоставленных типов.

jcalz 02.02.2023 22:07

Редактируя вопрос и ожидая любой информации, которую вы можете предоставить, я ломал голову над этим в течение последних 3 недель, 8-часовой рабочий день ... Я понимаю, что не силен в TS, но это какой-то следующий уровень взлом, редактирую сейчас, скоро будет

Dellirium 02.02.2023 22:08

Хорошо, я сделаю это, когда у меня будет шанс; это может быть не в течение нескольких часов.

jcalz 02.02.2023 22:55

Перемещение по общему правилу, предложенное @jcalz, должно решить эти проблемы. Каков фактический вариант использования? А что не работает со встроенным redux типа StoreEnhancer? По общему признанию, я никогда не пишу энхансеры, поэтому у меня нет опыта использования этого типа. Меня смущает, что и next, и возвращаемый тип являются расширенной версией, но я не полностью окунулся в нее.

Linda Paiste 02.02.2023 23:46

На самом деле я получил кучу ошибок, когда копировал и вставлял тестовый код redux Enhancer в TS Playground, так что вы можете что-то здесь понять.

Linda Paiste 02.02.2023 23:47

Существует множество проблем с исходной версией Redux, как она есть, с ней ужасно сложно работать, она лжет о том, что она на самом деле делает, она не описывает, что на самом деле делает код js в некоторых случаях. И это только в том случае, если вы хотите использовать его как есть. Я хочу расширить его, что делает все это моим худшим кошмаром... Но мне это нужно, чтобы создать безопасную для типов среду для полностью развязанной модульной структуры (какой-то)...

Dellirium 03.02.2023 01:18

@jcalz Я обновил вопрос и с нетерпением жду вашего ответа с максимально возможными объяснениями / подробностями. Код, который вы мне дали, работает, но, на мой взгляд, это выглядит как чрезвычайно хакерский способ передачи информации компилятору TS, информацию, которую он должен иметь возможность получить другими способами, но, по-видимому, это работает, поэтому я больше не знаю.

Dellirium 03.02.2023 01:20

Ну вот. Некоторые из них представляют собой несколько продвинутое жонглирование типа TS, но я бы сказал, что «хакерство» в глазах смотрящего ... все здесь использует поддерживаемые функции, хотя TupleToIntersection<T> немного ближе к краю, чем все остальное.

jcalz 03.02.2023 04:50

Привет, народ. Из любопытства, какой фактический вариант использования вызвал этот вопрос в первую очередь? Почему вы хотите написать свой собственный метод composeEnhancers? Какую проблему ты пытаешься решить? @dellirium, можете ли вы уточнить, что вы подразумеваете под «оригинальным compose Redux ложью, и с ним трудно работать»? Я не слышал, чтобы кто-то говорил что-то подобное раньше.

markerikson 04.02.2023 02:31

Все это является лишь частью «улучшений», которые я делаю при переупаковке избыточности в более «отдельную» версию в комплекте. Идея/пункт состоит в том, чтобы позволить нескольким меньшим "связкам" писаться отдельно и иметь их "предварительно скомпилированные" (из-за отсутствия лучшего слова) в единое хранилище, которое вы можете использовать внутри приложения. Я уже сделал это на простом JavaScript, но очевидно, что с TS есть дополнительное преимущество, заключающееся в возможности IntelliSense структуры, и, таким образом, это делает DevExp лучше. Продолжение в следующем комментарии:

Dellirium 08.02.2023 15:31

Проблема с использованием обычного компоновщика заключается в том, что он просто не может передавать информацию о типе. Проблема со всеми остальными частями Redux в том, что они неверны или неполны. Проблема энхансеров магазина с PR Линдой, упомянутая выше, — это только вершина айсберга, реализация createStore не соответствует типам, это просто огромная головная боль. Даже не заводите меня о промежуточном программном обеспечении... Предполагается, что промежуточное программное обеспечение может перехватывать действия, но вы не можете сделать свои действия чем-либо, что не расширяет базовое действие.

Dellirium 08.02.2023 15:41
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
21
61
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Цель состоит в том, чтобы написать функцию 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, аргумент типа которого является пересечением аргументов типа входных StoreEnhancers.


И мы, по сути, закончили. Функцию все еще нужно реализовать, и реализация достаточно проста, но, к сожалению, поскольку сигнатура вызова довольно сложна, нет надежды, что компилятор сможет проверить, действительно ли реализация полностью соответствует сигнатуре вызова:

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, но безрезультатно. Я хотел бы спросить еще одну вещь (но не хватает места для этого комментария)

Dellirium 03.02.2023 05:11

«Трюк», используемый для отображения кортежа, немного «обратный» в моей голове, вы сказали, что тип T — это кортеж [X, Y, Z], который отображается на входы, что является самым встречным -интуитивный способ взглянуть на это с моей точки зрения, но я понимаю, как это работает. Можно ли использовать тот же трюк для «извлечения» информации о типах возвращаемых значений из массива функций, возвращающих простые объекты, а затем, конечно же, смешать эти объекты вместе... и пока я пишу это, я понимаю, да, это было бы... Чувак, это какой-то следующий уровень $h!7. Спасибо за объяснение, очень признателен.

Dellirium 03.02.2023 05:15

Возможно, но без минимального воспроизводимого примера точно говорить не хотелось бы. В любом случае мне нужно расписаться на ночь.

jcalz 03.02.2023 05:17

Другие вопросы по теме