Я реализую типобезопасную функцию shapeAdapter
в TypeScript, которая принимает исходный объект, функцию adapt
для его преобразования и функцию revert
для его обратного преобразования.
function shapeAdapter<Original, Transformed>(props: {
original: Original;
adapt: (original: NoInfer<Original>) => Transformed;
revert: (transformed: NoInfer<Transformed>) => NoInfer<Original>;
}) {
throw new Error("Implementation does not matter")
}
Функция работает так, как ожидалось, если adapt
объявлено перед revert
:
shapeAdapter({
original: { value: 1 },
adapt: (original) => original.value,
revert: (transformed) => ({ value: transformed }), // transformed is of type number as expected
})
Однако, когда я меняю порядок adapt
и revert
, вывод типа нарушается:
shapeAdapter({
original: { value: 1 },
revert: (transformed) => ({ value: transformed }), // "transformed" is of type unknown! should be number
adapt: (original) => original.value,
})
Во втором примере transformed
в функции revert
выводится как unknown
вместо number
.
Я использую служебный тип NoInfer
, чтобы предотвратить чрезмерное расширение типа, но это не решает эту проблему.
Есть ли способ обеспечить правильный вывод типа независимо от порядка этих обратных вызовов?
Версия TypeScript: 5.5.2
Ссылка на игровую площадку TS Playground
Что я пробовал:
Первоначально я реализовал функцию с adapt
перед revert
, которая работала, как и ожидалось, но затем я попытался изменить порядок аргументов, поместив revert
перед adapt
, но это нарушило вывод типа, затем я попытался использовать тип утилиты NoInfer
для общих параметров, чтобы предотвратить чрезмерное расширение шрифта, но это тоже не помогло.
Чего я ожидал:
Я ожидал, что TypeScript сможет определить связь между типами Original
и Transformed
на основе структуры функции shapeAdapter
, независимо от порядка обратных вызовов.
Да, это решает вопрос, спасибо за ссылку
К сожалению, это (в настоящее время) невозможно. У TypeScript всегда были проблемы с возможностью одновременного вывода как аргументов общего типа, так и типов параметров обратного вызова из контекста . Существует запрос на открытую функцию по адресу microsoft/TypeScript#47599, но он почти наверняка никогда не будет полностью решен для всех вариантов использования.
До того, как в TypeScript 4.7 появилась поддержка вывода дженериков и параметров обратного вызова из объектных литералов , даже ваш рабочий кейс мог сломаться. (Вы можете убедиться в этом, заменив NoInfer на свое собственное определение, поместив его в IDE с TS4.6 и убедитесь сами .) Имеющаяся у нас поддержка была реализована в microsoft/TypeScript. #48538 и там конкретно сказано, что
Информация о выведенном типе передается только слева направо между контекстно-зависимыми аргументами. Это давнее ограничение нашего алгоритма вывода, и оно вряд ли изменится.
«Слева направо» означает, что порядок свойств в литерале вашего объекта имеет значение, когда дело доходит до вывода. Вы хотите упомянуть «известные» вещи раньше, чем все, что нужно сделать. Это очень похоже на то, как все будет вести себя, если вы разделите свой объект на отдельные параметры функции (информация о выведенном типе передается от более ранних типов параметров к более поздним типам параметров):
function shapeAdapterGood<O, T>(
original: O,
adapt: (o: NoInfer<O>) => T,
revert: (t: NoInfer<T>) => NoInfer<O>
) { }
shapeAdapterGood({ value: 1 }, o => o.value, t => ({ value: t }))
function shapeAdapterBad<O, T>(
original: O,
revert: (t: NoInfer<T>) => NoInfer<O>,
adapt: (o: NoInfer<O>) => T
) { }
shapeAdapterBad({ value: 1 }, t => ({ value: t }), o => o.value) // error!
Или как все будет вести себя, если вы каррируете функцию (информация о типе передается от более ранних вызовов к более поздним):
const shapeAdapterCurryGood =
<O,>(original: O) =>
<T,>(adapt: (o: NoInfer<O>) => T) =>
(revert: (t: NoInfer<T>) => NoInfer<O>) => { }
shapeAdapterCurryGood({ value: 1 })(o => o.value)(t => ({ value: t }))
const shapeAdapterCurryBad =
<O,>(original: O) =>
<T,>(revert: (t: NoInfer<T>) => NoInfer<O>) =>
(adapt: (o: NoInfer<O>) => T) => { };
shapeAdapterCurryBad({ value: 1 })(t => ({ value: t }))(o => o.value) // error!
С помощью этой последней формулировки должно быть очевидно, почему вывод не работает; TypeScript пришлось бы каким-то образом отложить вывод T
до тех пор, пока не будет вызвана вторая функция, но к тому времени уже слишком поздно. Конечно, вы можете надеяться, что использование объектов в виде литерала объекта даст TypeScript больше возможностей для такого вывода, но это не так. TypeScript в основном оптимизирован для вывода слева направо.
Ответ, к сожалению, нет. TS вообще может это сделать только благодаря поддержке , добавленной в TS 4.7 , реализованной в ms/TS#48538. В нем говорится: «Информация о выведенном типе передается только слева направо между контекстно-зависимыми аргументами. Это давнее ограничение нашего алгоритма вывода, и оно вряд ли изменится». Это полностью решает вопрос? Если да, я напишу ответ с объяснением; если нет, то что мне не хватает?