Я пытаюсь обернуть функцию обратного вызова в 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);
});
}
Однако я получаю следующие ошибки:
введите сюда описание изображения
Argument of type '(...args: Parameters<Callbacks[T]>) => void' is not assignable to parameter of type 'Callbacks[T]'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);
});
}
Этот подход работает без каких-либо ошибок. Но мне не нравится определять все подписи через остальные параметры.
Почему первый подход терпит неудачу из-за ошибок типа и почему использование остальных параметров во втором подходе решает эти проблемы?
@jcalz Думаю, это касается вопроса, но дело в том, что я бы хотел, чтобы это работало без этого обходного пути. Согласны ли вы, что с логической точки зрения это должно позволять распространять параметры<T> на исходные аргументы cb? Если это правда, это означает, что компилятор TS игнорирует логику в этом случае. Итак, я пытаюсь понять, есть ли другой способ реализовать это или это, вероятно, будет исправлено в следующих версиях TS.
Я понимаю, что вам бы хотелось, чтобы он вёл себя по-другому, но это не так, и я объяснил почему. TS не является решателем доказательств. Возможно, вы бы сказали, что она «игнорирует» логику, точно так же, как птица «игнорирует» знак «не приближаться к траве». Вы ожидаете слишком многого от компилятора. См. ms/TS#47615 и ms/TS#42818. Я что-то упустил или мне стоит написать ответ?
Хорошо, вы можете написать ответ и упомянуть проблемы с гормоном роста.
@jcalz Спасибо за ответ! Что касается вашего замечания о птицах, я не совсем согласен. Это логично так же, как не бывает женатых холостяков, как и математическая логика. Не существует юниверса, в котором (...args: Параметры<Callbacks[T]>) => void нельзя было бы назначить для Callbacks[T], если бы Callbacks определялись так, как есть, а именно {[key]: someFunction}. В прошлом было много случаев, когда TS не видел очевидной логики, но в новой версии это исправится. Так что думаю надежда есть :)
Вы имеете в виду, что ожидаете, что TypeScript сможет проверять истинность всех утверждений, которые являются логическими тавтологиями? Или только те, которые вы считаете «очевидными»? Если вы не знаете, что означает слово «холостяк», вы, вероятно, не сможете легко прийти к выводу, что фраза «все холостяки не женаты» — это правда. ТС не знает, что значит Parameters.






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