Преобразование типов аргументов функции и несоответствие между сопоставленными типами (работает) и рекурсивным типом (не работает)

Я возился с преобразованием аргументов функции через рекурсивный тип, у меня возникла проблема с тем, чтобы тип работал, я сделал тот же упрощенный пример, чтобы подчеркнуть проблему, вместе с примерами с использованием сопоставленного типа (для этого игрушечного примера сопоставленного типа было бы достаточно): Детская площадка

Сначала я просто создаю функции Transform, используя как отображаемые типы, так и рекурсивные типы, с тестовыми данными и тестами, чтобы гарантировать, что они возвращают один и тот же результат.

type TestItems = [string, number, string, boolean]

export type MappedTransform<UnionWith, Items extends any[]> 
  = { [I in keyof Items]: Items[I] | UnionWith }

type RecursiveTransform<UnionWith, Items extends any[]> 
  = Items extends [infer Head, ...infer Tail]
  ? [Head | UnionWith, ...RecursiveTransform<UnionWith, Tail>]
  : [];

type mappedTypes = MappedTransform<undefined, TestItems>

type recursiveTypes = RecursiveTransform<undefined, TestItems>

Затем у меня есть функция, которая использует преобразование через сопоставленный тип. Я предполагаю, что тип UnionWith подключается в качестве аргумента первого типа, а второй аргумент заполняется типом кортежа с типами аргументов функции. Это создает правильный тип.

export const fnWithMappedType = <UnionWith extends any, Args extends any[]>
  (data: UnionWith, ...args: MappedTransform<UnionWith, Args>) => {}

fnWithMappedType(undefined, "first", 42, true)

Далее следует проблема: типы, подключенные к рекурсивному преобразователю типов так же, как указано выше, к типу, который ведет себя одинаково изолированно, в тестовых примерах ведут себя по-разному и приводят к пустому кортежу, так что функция принимает только самый первый параметр в отличие от всех четыре в предыдущем примере.

export const fnWithRecursiveType = <UnionWith extends any, Args extends any[]>
  (data: UnionWith, ...args: RecursiveTransform<UnionWith, Args>) => {}

fnWithRecursiveType(undefined, "first", 42, true)

Есть ли какая-то скрытая ошибка или я что-то пропустил, можно ли это решить, чтобы аргументы могли быть преобразованы рекурсивным типом?

«Скрытая ошибка» заключается в том, что TS может легко сделать вывод из гомоморфного отображаемого типа (и это «скрыто» только потому, что документации нет в текущем справочнике TS). Вы не можете ожидать, что TS обратит функцию произвольного типа и выведет входные данные из выходных. Если ваша функция — это просто проверка типа, вы можете использовать пересечение, например this; в противном случае вам придется написать обратное преобразование самостоятельно. Это полностью решает вопрос? Если да, то я напишу a; если нет, то что мне не хватает?

jcalz 04.07.2024 13:20

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

Jarek 05.07.2024 19:40

Когда у вас есть функция типа function f<T>(u: F<T>): void и вы вызываете ее с помощью f(u), где u имеет тип U, вы надеетесь, что компилятор сможет начать с «U = F<T>» и инвертировать функцию типа F, чтобы он мог выяснить, что такое T. Это обратное отображение, а не что-то вообще возможное. В частности, хорошо поддерживаемые ситуации, такие как гомоморфные отображаемые типы. Вы согласны, что я затронул вопрос? Если да, то я напишу фактическое объяснение в реальном ответе. В противном случае я что-то упускаю и хотел бы услышать, что это такое. Подскажите, продолжать или нет?

jcalz 05.07.2024 19:56

Да, это было хорошо объяснено с вашей стороны, спасибо.

Jarek 06.07.2024 09:07

Я напишу ответ, когда у меня будет возможность.

jcalz 06.07.2024 13:50
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
5
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

В общем, TypeScript не может выводить общие аргументы типа из функций типа по соответствующему параметру типа. То есть, если у вас есть универсальная функция, которая выглядит так

declare function fn<T>(u: F<T>): T;

где F<T> — это функция некоторого типа, например type F<T> = ⋯, и вы вызываете эту функцию с аргументом некоторого типа U, например:

declare const u: U;
const t = fn(u);

тогда вы просите TypeScript инвертировать F<T>. Это означает, что вы хотите, чтобы TypeScript вычислял что-то вроде type F⁻¹<U> = ⋯, чтобы F<F⁻¹<U>> было U независимо от того, что такое U.

Но TypeScript вообще не может этого сделать. Существуют только определенные формы функций типов. TypeScript умеет инвертировать. Даже если игнорировать случаи, когда инвертировать даже в принципе невозможно (например, type F<T> = string, который не зависит от T), функции типов могут быть очень сложными, и инвертировать их — дело непростое. Особенно, если эти функции типов включают условные типы. Представлять себе

type F<T extends string, A extends string = ""> =
  T extends `${infer L}.${infer R}` ? F<R, `${L}${A}${L}`> : `${T}${A}${T}`

Что такое type F⁻¹<U> = ? Если бы я сказал вам, что U — это "zzzazazazazzazzzza", насколько легко вам было бы понять, что такое T? Это не невозможно (на самом деле существует несколько возможностей, но есть одна «лучшая»), но мы не можем ожидать, что TypeScript просто сделает это.


Одним из поддерживаемых случаев является случай, когда F<T> является гомоморфным отображаемым типом (см. Что означает «гомоморфный отображаемый тип»? ) формы type F<T> = {[K in keyof T]: ⋯T[K]⋯}. Это описано в TypeScript Handbook v1 как «вывод из сопоставленных типов», но, похоже, не упоминается в текущей версии TypeScript Handbook. В любом случае, это легко сделать компилятору, потому что он может сделать вывод из F<T>, если он может сделать вывод из свойств F<T>, поскольку каждое свойство на входе преобразуется непосредственно в свойство на выходе. Поскольку отображаемые типы преобразуют кортежи в кортежи, это означает, что для типа кортежа T можно вывести из F<T> поэлементно. Итак MappedTransform работает.

С другой стороны, ваш RecursiveTransform этого не делает, потому что вы просите TypeScript выполнить итерацию в обратном направлении через рекурсивный условный тип. В целом это просто не работает, а TypeScript даже не пытается. Вывод не удался, и вы получаете то поведение, которое видите. Здесь вы можете либо вручную инвертировать функцию типа, чтобы что-то, начинающееся с

declare const fnWithRecursiveType:
  <U extends any, A extends any[]>(data: U, ...args: RecursiveTransform<U, A>) => A

изменился бы на

type InvertedRecursiveTransform<U, V> = ⋯

declare const fnWithRecursiveType:
  <U extends any, V extends any[]>(data: U, ...args: V) => InvertedRecursiveTransform<U, V>

ИЛИ, если ваше рекурсивное преобразование не выполняется, когда A допустимо (то есть, если A extends RecursiveTransform<U, A> когда A правильно), то вы можете использовать пересечение (это способ обойти ms/TS#7234) формы

declare const fnWithRecursiveType:
  <U extends any, A extends any[]>(data: U, ...args: A & RecursiveTransform<U, A>) => A;

Детская площадка, ссылка на код

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