Function, Functional Types

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

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

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

ts
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, также существует функциональный тип, с помощью которого осуществляется описание сигнатур функциональных выражений.

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

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

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

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

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

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

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

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

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

ts
// .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, что лишает ide автодополнения. Для таких и не только случаев была реализованна возможность декларировать тип this непосредственно в функциях.

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

ts
interface IT1 {p1: string;}

function f1(this: IT1): void {}

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

ts
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, который может похвастаться, что ни разу не сталкивался с потерей контекста при передаче метода объекта в качестве функции обратного вызова (callback). В случаях, когда в теле метода происходит обращение через ссылку this к членам объекта, в котором он определен, то при потере контекста, в лучшем случае возникнет ошибка. В худшем, предполагающем, что в новом контексте будут присутствовать схожие признаки, возникнет трудно выявляемая ошибка.

ts
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.

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

ts
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 в самом слушателе событий.

ts
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.