Я возился с преобразованием аргументов функции через рекурсивный тип, у меня возникла проблема с тем, чтобы тип работал, я сделал тот же упрощенный пример, чтобы подчеркнуть проблему, вместе с примерами с использованием сопоставленного типа (для этого игрушечного примера сопоставленного типа было бы достаточно): Детская площадка
Сначала я просто создаю функции 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)
Есть ли какая-то скрытая ошибка или я что-то пропустил, можно ли это решить, чтобы аргументы могли быть преобразованы рекурсивным типом?
Я пытался выполнить что-то вроде операции конвейера, когда вы передаете элемент данных первой функции, которая, в свою очередь, передает тип возвращаемого значения следующей в цепочке и так далее. Я знаю, что это можно легко сделать, определив типы перегруженных функций, но только определив их вручную. Спасибо за этот совет об устаревших документах, он был полезен. Что вы имели в виду под обратным преобразованием? Не могли бы вы уточнить?
Когда у вас есть функция типа function f<T>(u: F<T>): void
и вы вызываете ее с помощью f(u)
, где u
имеет тип U
, вы надеетесь, что компилятор сможет начать с «U = F<T>
» и инвертировать функцию типа F
, чтобы он мог выяснить, что такое T
. Это обратное отображение, а не что-то вообще возможное. В частности, хорошо поддерживаемые ситуации, такие как гомоморфные отображаемые типы. Вы согласны, что я затронул вопрос? Если да, то я напишу фактическое объяснение в реальном ответе. В противном случае я что-то упускаю и хотел бы услышать, что это такое. Подскажите, продолжать или нет?
Да, это было хорошо объяснено с вашей стороны, спасибо.
Я напишу ответ, когда у меня будет возможность.
В общем, 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;
«Скрытая ошибка» заключается в том, что TS может легко сделать вывод из гомоморфного отображаемого типа (и это «скрыто» только потому, что документации нет в текущем справочнике TS). Вы не можете ожидать, что TS обратит функцию произвольного типа и выведет входные данные из выходных. Если ваша функция — это просто проверка типа, вы можете использовать пересечение, например this; в противном случае вам придется написать обратное преобразование самостоятельно. Это полностью решает вопрос? Если да, то я напишу a; если нет, то что мне не хватает?