Перейти к содержанию

Function, Functional Types

Функция — это ключевая концепция JavaScript. Функции присваиваются в качестве значений переменным и передаются как аргументы при вызове других функций. Поэтому не удивительно, что в TypeScript существует возможность декларировать функциональный тип.

Function Types - тип функция

В TypeScript, так же как и в JavaScript, существует объектный тип Function. Поскольку в JavaScript все функции принадлежат к типу Function, в TypeScript тип Function является базовым для всех функций. Тип Function можно указывать в аннотации типа тогда, когда о сигнатуре функции ничего не известно или в качестве значения могут выступать функции с несовместимыми сигнатурами.

function f1(p1: number): string {
  return p1.toString()
}

function f2(p1: string): number {
  return p1.length
}

let v1: Function = f1
let v2: Function = f2

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

Поведение типа Function идентично одноимённому типу из JavaScript.

Functional Types - функциональный тип

Помимо того, что в TypeScript существует объектный тип Function, также существует функциональный тип, с помощью которого осуществляется описание сигнатур функциональных выражений.

Функциональный тип обозначается с помощью пары круглых скобок (открывающая и закрывающая), после которых идет стрелка, после которой обязательно указывается тип возвращаемого значения. При наличии у функционально выражения параметров, их декларация заключается между круглых скобок (p1: type, p2: type) => type.

type FunctionalType = (p1: type, p2: type) => type

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

type SumFunction = (a: number, b: number) => number

const sum: SumFunction = (a: number, b: number): number =>
  a + b

Поведение функционального типа указывающегося с помощью функционального литерала идентично поведению типа Function, но при этом более оно более конкретно и поэтому предпочтительнее.

this в сигнатуре функции

Ни для кого не будет секретом, что в JavaScript при вызове функций можно указать их контекст. В львиной доле случаев, возможность изменять контекст вызова функции является нежелаемым поведением JavaScript, но только не в случае реализации такой конструкции, как функциональная примесь (functional mixins).

Функциональная примесь — это функция, которая в своем теле ссылается на члены объявленные в объекте, к которому она “примешивается”. Проблем не возникнет, если подобный механизм реализуется в динамически типизированном языке, каким является JavaScript.

// .js

class Animal {
  constructor() {
    this.type = 'animal'
  }
}

function getType() {
  return this.type
}

let animal = new Animal()
animal[getType.name] = getType

console.log(animal.getType()) // animal

Но в статически типизированном языке такое поведение должно быть расценено, как ошибочное, так как у функции нет присущего объектам признака this. Несмотря на это, в JavaScript, а значит и в TypeScript, контекст самой программы (или, по другому, глобальный объект) является объектом. Это в свою очередь означает, что не существует места, в котором бы ключевое слово this привело к возникновению ошибки (чтобы запретить указывать this в нежелательных местах, можно активировать опцию компилятора --noImplicitThis). Но при этом, за невозможностью предугадать поведение разработчика, в TypeScript ссылка this вне конкретного объекта ссылается на тип any, что не дает редактору кода выводит автодополнение кода. Для таких и не только случаев была создана возможность декларировать тип this в функциях.

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

interface IT1 {
  p1: string
}

function f1(this: IT1): void {}

Несмотря на то, что this декларируется в параметрах функции, таковыми оно не считается. Поведение функции с декларацией this аналогично поведению функции без декларации this. Единственное, на что стоит обратить внимание, что в случае указания принадлежности к типу, отличного от void, не получится вызвать функцию вне указанного контекста.

interface IT1 {
  p1: string
}

function f1(this: void): void {}
function f2(this: IT1): void {}
function f3(): void {}

f1() // Ok
f2() // Error
f3() // Ok

let v1 = {
  // v1: {f2: (this: IT1) => void;}
  f2: f2,
}

v1.f2() // Error

let v2 = {
  // v2: {p1: string; f2: (this: IT1) => void;}
  p1: '',
  f2: f2,
}

v2.f2() // Ok

Кроме того, возможность ограничивать поведение ключевого слова this в теле функции призвано частично решить самую часто возникающую проблему, связанную с потерей контекста. Вряд ли найдется разработчик JavaScript, который может похвастаться, что ни разу не сталкивался с потерей контекста при установке метода объекта в качестве слушателя событий. Ошибка возникает тогда, когда в теле слушателя события происходит обращение к членам через ссылку this. Происходит это потому, что у вызывающего контекста отсутствуют признаки контекста, в котором изначально объявлялся слушатель. В редких случаях, когда у контекста вызова слушателя событий будут присутствовать признаки первоначального контекста, возникнет не ожидаемое поведение, причину которого очень трудно выявить.

class Point {
  constructor(public x: number = 0, public y: number = 0) {}
}

class Animal {
  private readonly position: Point = new Point()

  public move({ clientX, clientY }: MouseEvent): void {
    this.position.x = clientX
    this.position.y = clientY
  }
}

let animal = new Animal()

document.addEventListener('mousemove', animal.move) // ошибка во время выполнения

В случаях, когда создаются объекты, содержащие объявления методов, которые в качестве аргументов принимают слушателей событий, TypeScript предлагает ограничить ссылку на контекст с помощью декларации this.

Так как реальный пример иллюстрирующий полную картину получается очень объемным, то ограничимся одним методом, реализующим обсуждаемое поведение.

type IContextHandler = (
  this: void,
  event: MouseEvent
) => void

class Controller {
  public addEventListener(
    type: string,
    handler: IContextHandler
  ): void {}
}

let animal = new Animal()
let controller = new Controller()

controller.addEventListener('mousemove', animal.move) // ошибка во время выполнения

Стоит заметить, что одной декларации ссылки this в слушателе событий недостаточно. Для того, чтобы пример заработал так, как было заявлено в начале, нужно задекларировать ссылку this в самом слушателе событий.

class Point {
  constructor(public x: number = 0, public y: number = 0) {}
}

class Animal {
  private readonly position: Point = new Point()

  public move(
    this: Animal,
    { clientX, clientY }: MouseEvent
  ): void {
    // <= изменения
    this.position.x = clientX
    this.position.y = clientY
  }
}

type IContextHandler = (
  this: void,
  event: MouseEvent
) => void

class Controller {
  public addEventListener(
    type: string,
    handler: IContextHandler
  ): void {}
}

let animal = new Animal()
let controller = new Controller()

controller.addEventListener('mousemove', animal.move) // ошибка во время компиляции
controller.addEventListener('mousemove', (event) =>
  animal.move(event)
) // Ok

Также стоит обратить внимание на одну неочевидную, на первый взгляд, деталь. Когда мы передаем слушатель, обернув его в стрелочную функцию, либо в метод функции .bind, ошибки не возникает только потому, что у передаваемой функции отсутствует декларация this.

Итог

  • Ссылочный тип Function является базовым типом для всех функций и указывается в аннотации только тогда, когда при объявлении не присваивается функциональное выражение, но при этом нужно указать принадлежность значения к типу Function.
  • Функциональный тип указывается с помощью круглых скобок и обязательного возвращаемого типа, разделенных стрелкой. При наличии параметров они декларируются в круглых скобках.
  • Тогда, когда декларация сигнатуры функционального выражения не известна, можно указывать тип глобального интерфейса Function.
  • Имена параметров функциональных типов не участвуют при проверке на совместимость.
  • Любой тип возвращаемого значения совместим с типом void, но не наоборот.
  • Два функциональных типа считаются совместимыми, если их сигнатуры имеют совместимые типы. При этом у типа, выступающего в роли значения, может быть меньшее число параметров, при условии, что их типы будут совместимы с типами другого функционального типа, в порядке их объявления.