Почему TypeScript жалуется на расширение аргументов в оболочках обратного вызова

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

Вот интерфейс Callbacks:

interface Callbacks {
  foo: (a: string, b: string) => void;
  bar: (x: number) => void;
  baz: (z: boolean) => void;
}

type CbType = keyof Callbacks;

Я попытался реализовать функции trigger и wrapTrigger следующим образом:

function trigger<T extends CbType>(name: T, cb: Callbacks[T]) {
  // do stuff
}

function wrapTrigger<T extends CbType>(name: T, cb: Callbacks[T]) {
  return trigger(name, (...args: Parameters<Callbacks[T]>) => {
    console.info('Triggered!');
    return cb(...args);
  });
}

Однако я получаю следующие ошибки:

введите сюда описание изображения

  1. Argument of type '(...args: Parameters<Callbacks[T]>) => void' is not assignable to parameter of type 'Callbacks[T]'
  2. A spread argument must either have a tuple type or be passed to a rest parameter.

Чтобы обойти эту проблему, я модифицировал функцию trigger, чтобы она принимала обратные вызовы с параметрами rest, и она работает:

function triggerSpread<T extends CbType>(name: T, cb: (...args: Parameters<Callbacks[T]>) => ReturnType<Callbacks[T]>) {
  // do other stuff
}

function wrapTriggerSpread<T extends CbType>(name: T, cb: (...args: Parameters<Callbacks[T]>) => ReturnType<Callbacks[T]>) {
  return triggerSpread(name, (...args) => {
    console.info('Triggered!');
    return cb(...args);
  });
}

Этот подход работает без каких-либо ошибок. Но мне не нравится определять все подписи через остальные параметры.

Почему первый подход терпит неудачу из-за ошибок типа и почему использование остальных параметров во втором подходе решает эти проблемы?

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

Parameters<T> — условный тип; TS не знает, что это значит, он просто оценивает это, когда T относится к определенному типу. Когда T является универсальным (как в Callbacks[K]), оно откладывает оценку, поэтому TS не может проверить, можно ли назначить какое-либо значение. Ему не хватает концепции более высокого порядка, которую (...args: Parameters<T>) => void всегда можно присвоить T. Второй подход работает, поскольку тип параметра в обоих случаях идентичен, хотя идиоматический способ записи выглядит так: это. Это полностью решает вопрос? Если да, то я напишу a; если нет, то чего не хватает?
jcalz 28.08.2024 15:34

@jcalz Думаю, это касается вопроса, но дело в том, что я бы хотел, чтобы это работало без этого обходного пути. Согласны ли вы, что с логической точки зрения это должно позволять распространять параметры<T> на исходные аргументы cb? Если это правда, это означает, что компилятор TS игнорирует логику в этом случае. Итак, я пытаюсь понять, есть ли другой способ реализовать это или это, вероятно, будет исправлено в следующих версиях TS.

user27059843 28.08.2024 16:41

Я понимаю, что вам бы хотелось, чтобы он вёл себя по-другому, но это не так, и я объяснил почему. TS не является решателем доказательств. Возможно, вы бы сказали, что она «игнорирует» логику, точно так же, как птица «игнорирует» знак «не приближаться к траве». Вы ожидаете слишком многого от компилятора. См. ms/TS#47615 и ms/TS#42818. Я что-то упустил или мне стоит написать ответ?

jcalz 28.08.2024 17:01

Хорошо, вы можете написать ответ и упомянуть проблемы с гормоном роста.

user27059843 28.08.2024 17:10

@jcalz Спасибо за ответ! Что касается вашего замечания о птицах, я не совсем согласен. Это логично так же, как не бывает женатых холостяков, как и математическая логика. Не существует юниверса, в котором (...args: Параметры<Callbacks[T]>) => void нельзя было бы назначить для Callbacks[T], если бы Callbacks определялись так, как есть, а именно {[key]: someFunction}. В прошлом было много случаев, когда TS не видел очевидной логики, но в новой версии это исправится. Так что думаю надежда есть :)

user27059843 28.08.2024 20:19

Вы имеете в виду, что ожидаете, что TypeScript сможет проверять истинность всех утверждений, которые являются логическими тавтологиями? Или только те, которые вы считаете «очевидными»? Если вы не знаете, что означает слово «холостяк», вы, вероятно, не сможете легко прийти к выводу, что фраза «все холостяки не женаты» — это правда. ТС не знает, что значит Parameters.

jcalz 28.08.2024 20:26
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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
6
53
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

TypeScript, как правило, не способен абстрагировать факты о конкретных типах от их общих аналогов, особенно когда задействованы условные типы. Для любой функции f определенного типа TypeScript сможет увидеть, что f можно назначить (...args: Parameters<typeof f>) => void:

function foo(x: string, y: number) { return x.toUpperCase() + y.toFixed() }
const alsoFoo: (...args: Parameters<typeof foo>) => void = foo; // okay

function bar(z: boolean) { return z ? 0 : 1 }
const alsoBar: (...args: Parameters<typeof bar>) => void = bar; // okay

Но когда f имеет общий тип F, TypeScript не может этого увидеть:

function oops<F extends (...args: any) => any>(f: F) {
  const alsoF: (...args: Parameters<typeof f>) => void = bar; // error!
}

Это потому, что, хотя вы понимаете значение типа утилиты «Параметры<T>», TypeScript — нет. Это всего лишь условный тип. Когда T является общим, TypeScript даже не пытается увидеть через него какую-то истину более высокого порядка о функциях. Там просто написано: «Я еще не знаю, что такое T, поэтому я тоже пока не могу понять, что такое Parameters<T>». Это просто откладывает оценку. Поэтому он не может видеть, какие значения можно или нельзя ему присвоить.

Это ограничение дизайна TypeScript, как описано в microsoft/TypeScript#47615 и microsoft/TypeScript#42818. Сомнительно, что любая будущая версия TypeScript позволит этому работать автоматически.

Если вы хотите, чтобы TypeScript считал некоторые типы одинаковыми, вам нужно написать их одинаково. Ваш «распространенный» код, в котором оба упоминания обратного вызова по существу имеют тип (...args: Parameters<Callbacks[T]>) => void, работает следующим образом.


Стандартный рефакторинг такого рода корреляции описан в microsoft/TypeScript#47109 , где есть «базовый» интерфейс, связывающий ключи и некоторый базовый тип данных, а затем все ваши операции записываются в терминах этого базового интерфейса, или с точки зрения отображаемых типов в этот интерфейс, или с точки зрения общего индексированного доступа к этим типам.

Для вашего примера это выглядит так

interface CallbackParams {
  foo: [a: string, b: string];
  bar: [x: number];
  baz: [z: boolean];
}

type Callback<K extends keyof CallbackParams> =
  { [P in K]: (...args: CallbackParams[P]) => void }[K]

function trigger<K extends keyof CallbackParams>(
  name: K,
  cb: Callback<K>
) {
  // do stuff
}

function wrapTrigger<K extends keyof CallbackParams>(
  name: K,
  cb: Callback<K>
) {
  return trigger(name, (...args) => {
    console.info('Triggered!');
    return cb(...args);
  });
}

который похож на вашу «распространенную» версию. (Есть ситуации, когда важно, чтобы Callback<K> был типом распространяемого объекта, а не просто (...args: CallbackParams[K]) => void, но здесь это не имеет значения. Для получения более подробной информации см. microsoft/TypeScript#47109).

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

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