Совместимость функциональных типов¶
После объектных типов, мир совместимости функциональных типов может показаться перевернутым с ног на голову. Он обладает множеством нюансов, каждый из которых будет детально рассмотрен в текущей главе.
Важно¶
Важной частью работы с функциями является понимание совместимости функциональных типов. Поверхностное понимание механизма совместимости функциональных типов может сложить ошибочное чувство их постижения, поскольку то, что на первый взгляд кажется очевидным, не всегда может являться таковым. Для того чтобы понять замысел создателей TypeScript, нужно детально разобрать каждый момент. Но прежде стоит уточнить одну деталь. В примерах, которые будут обсуждаться в главе, посвященной типизации функциональных типов, будет использоваться уточняющий шаблон : Target = Source
. Кроме того, объектные типы, указанные в сигнатуре функции, ведут себя так же, как было описано в главе, посвященной совместимости объектных типов.
Совместимость параметров¶
Первое, на что стоит обратить внимание, это параметры функции. На параметры функции приходится наибольшее количество неоднозначностей, связанных с совместимостью.
Начать стоит с того, что две сигнатуры считаются совместимыми, если они имеют равное количество параметров с совместимыми типами данных.
1 2 3 4 |
|
При этом стоит заметить, что идентификаторы параметров не участвуют в проверке на совместимость.
1 2 3 |
|
Кроме того, параметры, помеченные как необязательные, учитываются только тогда, когда они участвуют в проверке на совместимость по признакам количества параметров.
1 2 3 4 5 6 |
|
Функция, имеющая определение остаточных параметров, будет совместима в обе стороны с любой функцией, так как остаточные параметры расцениваются способными принадлежать к любому типу и чье количество находится в диапазоне от нуля до бесконечности.
1 2 3 4 5 6 7 8 |
|
В случае, если перед остаточными параметрами объявлены обязательные параметры, то функция будет совместима с любой другой функцией, которая совместима с обязательной частью.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Следующий, один из неочевидных моментов совместимости функциональных типов, заключается в том, что сигнатура с меньшим числом параметров совместима с сигнатурой с большим числом параметров, но не наоборот. Это правило верно при условии соблюдения предыдущих правил, относящихся к совместимости типов.
1 2 3 4 5 6 7 |
|
На данный момент уже известно, что два объектных типа (:T = S
), на основании структурной типизации, считаются совместимыми, если в типе S
присутствуют все признаки типа T
. Помимо этого, тип S
может быть более специфичным, чем тип T
. Простыми словами, тип S
, помимо всех признаков, присущих типу T
, также может обладать признаками, которые в типе T
отсутствуют, но не наоборот. Если ещё более просто, то больший тип совместим с меньшим типом данных. В случае с параметрами функциональных типов, все с точностью наоборот.
1 2 3 4 5 |
|
Такое поведение проще всего объяснить на примере работы с методами массива. За основу будет взята декларация метода forEach
из библиотеки lib.es5.d.ts
.
1 |
|
В данном случае нужно обратить внимание на функциональный тип первого параметра, описывающего три других параметра.
1 |
|
Если бы функциональный тип с большим числом параметров не был совместим с функциональным типом с меньшим числом параметров, то при работе с методом forEach
, при необходимости только в одном, первом параметре, обязательно бы приходилось создавать callback со всеми тремя параметрами, что привело бы к излишнему коду.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Кроме того, обобщенные функции, чьим параметрам в качестве типа указан параметр типа, проверяются на совместимость по стандартному сценарию.
1 2 3 4 5 6 7 8 |
|
Также стоит знать, что параметры, принадлежащие к конкретным типам, совместимы с параметрами, которым в качестве типов указаны параметры типа, но не наоборот.
1 2 3 4 5 6 7 8 |
|
Помимо того, что две сигнатуры считаются совместимыми, если участвующие в проверке параметры принадлежат к одному типу, они также считаются совместимыми при совместимости типов этих параметров. Но с этим связана ещё одна неочевидность. Как известно, в контексте объектных типов, если тип T1
не идентичен полностью типу T2
, и при этом тип T1
совместим с типом T2
, то значит тип T2
будет совместим с типом T1
через операцию приведения типов.
1 2 3 4 5 6 7 8 9 10 11 |
|
С типами в аннотации параметров функций все то же самое, только не требуется явного преобразования типов. Такое поведение называется бивариантностью параметров и создано для того, чтобы сохранить совместимость с распространенными в JavaScript практиками. Подробно бивариантность была рассмотрена в главе Совместимость типов на основе вариантности.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Изменить поведение бивариантного сопоставления параметров можно с помощью опции компилятора --strictFunctionTypes
. Установив флаг --strictFunctionTypes
в true
, сопоставление будет происходить по контрвариантным правилам (глава Совместимость типов на основе вариантности).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Совместимость возвращаемого значения¶
Первое, на что стоит обратить внимание, это ожидаемое поведение при проверке на совместимость возвращаемых типов. Другими словами, две сигнатуры считаются совместимыми, если их типы, указанные в аннотации возвращаемого значения, совместимы по правилам структурной типизации, которой подчиняются все объекты в TypeScript.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Исключение из этого правила составляет примитивный тип данных void
. Как стало известно из главы, посвященной типу данных void
, в обычном режиме он совместим только с типами null
и undefined
, так как они являются его подтипами. При активной рекомендуемой опции --strictNullChecks
примитивный тип void
совместим только с типом undefined
.
1 2 |
|
Но это правило неверно, если тип void
указан в аннотации возвращаемого из функции значения. В случаях, когда примитивный тип void
указан в качестве возвращаемого из функции типа, он совместим со всеми типами без исключения.
1 2 3 4 5 6 |
|
Причину, по которой поведение типа void
при указании его в аннотации возвращаемого из функции значения было изменено, лучше рассмотреть на примере работы с массивом, а точнее его методом forEach
.
Предположим, есть два массива. Первый массив состоит из элементов, принадлежащих к объектному типу, у которого определено поле name
. Второй массив предназначен для хранения строк и имеет длину, равную 0
.
1 2 3 4 5 |
|
Задача заключается в получении имен объектов из первого массива с последующим сохранением их во второй массив.
Для этого потребуется определить стрелочную функцию обратного вызова (callback). Слева от стрелки будет расположен один параметр value
, а справа — операция сохранения имени во второй массив с помощью метода push
. Если обратиться к декларации метода массива forEach
, то можно убедиться, что в качестве функции обратного вызова этот метод принимает функцию, у которой отсутствует возвращаемое значение.
1 |
|
Но в нашем случае в теле функции обратного вызова происходит операция добавления элемента в массив. Результатом этой операции является значение длины массива. То есть, метод push
возвращает значение, принадлежащее к типу number
, которое в свою очередь возвращается из стрелочной функции обратного вызова, переданного в метод forEach
, у которого этот параметр задекларирован как функция, возвращающая тип void
, что противоречит возвращенному типу number
. В данном случае отсутствие ошибки объясняется совместимостью типа void
, используемого в функциональных типах, со всеми остальными типами.
1 2 3 4 5 6 7 8 9 10 11 |
|
И напоследок стоит упомянуть, что две обобщенные функции считаются совместимыми, если у них в аннотации возвращаемого значения указан параметр типа.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Кроме того, параметр типа совместим с любым конкретным типом данных, но не наоборот.
1 2 3 4 5 6 7 8 9 10 11 12 |
|