Вывод универсального типа не выполняется при переупорядочении аргументов в функции с несколькими универсальными обратными вызовами

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

Ответ, к сожалению, нет. TS вообще может это сделать только благодаря поддержке , добавленной в TS 4.7 , реализованной в ms/TS#48538. В нем говорится: «Информация о выведенном типе передается только слева направо между контекстно-зависимыми аргументами. Это давнее ограничение нашего алгоритма вывода, и оно вряд ли изменится». Это полностью решает вопрос? Если да, я напишу ответ с объяснением; если нет, то что мне не хватает?

jcalz 05.07.2024 14:53

Да, это решает вопрос, спасибо за ссылку

Gabriel Pureliani 05.07.2024 15:25
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
0
2
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

К сожалению, это (в настоящее время) невозможно. У 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 в основном оптимизирован для вывода слева направо.

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

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