====== Function, Functional Types ====== Функция — это ключевая концепция JavaScript. Функции присваиваются в качестве значений переменным и передаются как аргументы при вызове других функций. Поэтому не удивительно, что TypeScript очень много внимания уделяет возможностям функционального типа, к которым, начиная с текущей главы, повествование периодически будет возвращаться. ===== Function Types - тип функция ===== В TypeScript тип ''Function'' представляет собой одноименный JavaScript конструктор, являющийся базовым для всех функций. Тип ''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'', также существует функциональный тип, с помощью которого осуществляется описание сигнатур функциональных выражений. Функциональный тип обозначается с помощью пары круглых скобок ''()'', после которых располагается стрелка, а после неё обязательно указывается тип возвращаемого значения ''() => type''. При наличии у функционального выражения параметров, их декларация заключается между круглых скобок ''(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'', что лишает ide автодополнения. Для таких и не только случаев была реализована возможность декларировать тип ''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, который может похвастаться, что ни разу не сталкивался с потерей контекста при передаче метода объекта в качестве функции обратного вызова (callback). В случаях, когда в теле метода происходит обращение через ссылку ''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''.