При вызове функции с аргументом тип аргумента присваивается типу параметра. Другими словами, тип аргумента должен быть равен типу параметра или уже его.
Пример:
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
(параметр параметра обратного вызова).
Почему направление назначения параметров в обратном вызове инвертируется по сравнению со сценарием без обратного вызова?
Если вы видите, что задания идут «в противоположном направлении», можно сказать, что они «меняются в противоположном направлении». Действительно, типы функций контравариантны по своим типам ввода. Таким образом, функция формы (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()
методов).
Вот почему для входных функций все назначается в противоположном направлении. Они изменяются в противоположных направлениях из-за направления движения данных. Часть данных не должна быть шире коробки, в которую она помещается. Таким образом, вы всегда можете безопасно увеличить этот блок или уменьшить данные, но не наоборот. Для параметров функции поступают данные, а это означает, что функция может сделать входной параметр шире, но не уже.
Детская площадка, ссылка на код