При назначении обратных вызовов в TypeScript параметры назначаются в направлении, противоположном самому обратному вызову. Почему это?

При вызове функции с аргументом тип аргумента присваивается типу параметра. Другими словами, тип аргумента должен быть равен типу параметра или уже его.

Пример:

const foo = (bar: string | number) => {
  console.info(bar);
}

foo('bar');

Вышеупомянутое работает нормально, потому что 'bar' (тип строкового литерала) можно назначить string | number.

Теперь вместо этого рассмотрим сценарий обратного вызова:

const foo = (bar: (baz: string | number) => void) => {
  bar('baz');
  // We should probably also have a call with a numeric parameter here, like bar(42),
  // since the callback signature implies that it is also called that way.
}

const bar = (baz: string) => {}

foo(bar);

Мы получаем это сообщение об ошибке bar('baz'):

Argument of type '(baz: string) => void' is not assignable to parameter of type '(baz: string | number) => void'.
  Types of parameters 'baz' and 'baz' are incompatible.
    Type 'string | number' is not assignable to type 'string'.
      Type 'number' is not assignable to type 'string'.ts(2345)

Как видно, в случае обратного вызова TS пытается присвоить тип (baz: string) => void (тип аргумента) (baz: string | number) => void (тип параметра). Это было, как и ожидалось.

Однако параметр обратного вызова назначается в противоположном направлении: тип string | number (параметр аргумента обратного вызова) присваивается типу string (параметр параметра обратного вызова).

Почему направление назначения параметров в обратном вызове инвертируется по сравнению со сценарием без обратного вызова?


2
56
1

Ответ:

Решено

Если вы видите, что задания идут «в противоположном направлении», можно сказать, что они «меняются в противоположном направлении». Действительно, типы функций контравариантны по своим типам ввода. Таким образом, функция формы (x: X) => void присваивается (y: Y) => void, если Y присваивается X, а не наоборот. В частности, (baz: string | number) => void можно присвоить (baz: string) => void, но (baz: string) => void нельзя присвоить (baz: string | number) => void.

В этом можно убедиться, добавив в код дополнительную функциональность, соответствующую типам. Во-первых, внутри foo() параметр bar должен принимать string | number, поэтому вы можете вызвать bar() с любым строковым или числовым аргументом, который вам нужен. Давайте убедимся, что мы делаем и то, и другое:

const foo = (bar: (baz: string | number) => void) => {
    bar('baz');
    bar(123); // <-- this is okay too
}

Тогда bar принимает только аргумент baz типа string, а это означает, что baz можно рассматривать как строку, например, вызывая его метод toUpperCase():

const bar = (baz: string) => { baz.toUpperCase() } // <-- this is okay too

Сейчас,

foo(bar); // error
//  ~~~ <-- Type 'string | number' is not assignable to type 'string'.

выдает ту же ошибку компилятора (и с точки зрения системы типов ничего не изменилось). Из-за контравариантности сообщение об ошибке сообщает вам, что (baz: string) => void нельзя назначить (baz: string | number) => void, потому что string | number нельзя назначить string.

И если вы действительно запустите этот код, не исправив ошибку, вы получите ошибку времени выполнения, которая baz.toUpperCaseне является функцией.

Тот факт, что функция может обрабатывать string, не означает, что она может обрабатывать string | number.


С другой стороны, вполне нормально расширить ввод функции:

const qux = (baz: string | number | boolean) => {
    if (typeof baz === "string") {
        console.info(baz.toUpperCase())
    } else if (typeof baz === "number") {
        console.info(baz.toFixed())
    } else {
        console.info(baz === true)
    }
}

foo(qux); // okay

Это работает, потому что любая функция, которая может обрабатывать string | number | boolean, определенно может обрабатывать string | number. Ошибки выполнения не возникает, потому что внутри qux() нельзя просто написать baz.toUpperCase(), не проверив, является ли это string. (И если вы попытаетесь это сделать, вы получите ошибку компилятора, говорящую, что number и boolean не имеют toUpperCase() методов).


Вот почему для входных функций все назначается в противоположном направлении. Они изменяются в противоположных направлениях из-за направления движения данных. Часть данных не должна быть шире коробки, в которую она помещается. Таким образом, вы всегда можете безопасно увеличить этот блок или уменьшить данные, но не наоборот. Для параметров функции поступают данные, а это означает, что функция может сделать входной параметр шире, но не уже.

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