Полиморфный тип this¶
Как в предке описать тип потомка и при чем здесь полиморфный тип this — сможет ответить текущая глава.
this — как тип¶
В TypeScript существует возможность указывать в качестве типа полиморфный тип this .
Полиморфный тип — это тип, который представляет множество типов как единое целое.
Полиморфный тип this является совокупностью типов, определяющих тип экземпляра. Кроме того, полиморфный тип this доступен для указания только в классах и интерфейсах.
Чтобы понять, о чем идет речь, нужно представить два класса, связанных с помощью механизма наследования, и в каждом из них объявлен метод, возращаемое значение которого принадлежит к типу представляющего экземпляр класса, в котором он определен.
1 2 3 4 5 6 7 8 9 10 11 12 13
class Animal public sit(): Animal // реализация метода return this; > > class Bird extends Animal public fly(): Bird // дополнение супертипа специфичным методом return this; > >
Если создать экземпляр подкласса и вызвать по цепочке метод подкласса, а затем — суперкласса, то операция завершится успехом.
const bird: Bird = new Bird() .fly() // Ok, возвращает тип Bird .sit(); // Ok, возвращает тип Animal
Если попробовать изменить порядок вызова методов, то возникнет ошибка. Произойдет это по той причине, что метод, объявленный в суперклассе, возвращает значение, принадлежащее к типу самого суперкласса, который ничего не знает о методах, объявленных в его подтипах.
const bird: Bird = new Bird() .sit() // Ok, возвращает тип Animal .fly(); // Error, в типе Animal, возвращенном на предыдущем шаге, нет объявления метода fly
Можно, конечно, в качестве возвращаемого значения указать тип any , но, помимо того, что пропадет автодополнение, ещё и пострадает семантическая привлекательность кода.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
class Animal public sit(): any return this; > > class Bird extends Animal public fly(): any return this; > > let bird: Bird = new Bird() .fly() // Ok .sit(); // Ok bird = new Bird() .sit() // Ok .fly(); // Ok, работает, но так лучше не делать
TypeScript предлагает решить эту проблему с помощью полиморфного типа this . Ожидаемое поведение достигается за счет того, что полиморфный тип this является множеством типов, определяемым цепочкой наследования. Другими словами, тип this будет принадлежать к тому же типу, что и экземпляр подкласса, который принадлежит к типу подкласса и типу суперкласса одновременно.
Такое поведение называется F-ограниченный полиморфизм (F-bounded polymorphism).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
class Animal public sit(): this return this; > > class Bird extends Animal public fly(): this return this; > > let bird = new Bird() .fly() // Ok .sit(); // Ok bird = new Bird() .sit() // Ok, возвращает тип Bird .fly(); // Ok
Стоит отдельно подчеркнуть, что полиморфный тип this не принадлежит к типу класса или интерфейса, в котором указан. Полиморфный тип this может быть определен только на основе экземпляра класса. Проще говоря, полиморфный тип this принадлежит к типу своего экземпляра и может быть определен только в момент создания экземпляра. Также тип this совместим с типом any , а, при условии, что флаг —strictNullChecks установлен в false , ещё и к типам null и undefined . К тому же тип this совместим с типом экземпляра, ссылку на который можно получить с помощью ключевого слова this .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
class Animal public animalAll: this[] = []; // массив с полиморфным типом this constructor() this.add(new Animal()); // Error, так как на данном этапе не известно, к какому типу будет принадлежать полиморфный тип this this.add(this); // Ok > public add(animal: this): this this.animalAll.push(animal); return this; > > class Type static interface: Animal = new Animal(); static animal: Animal = new Animal(); static any: any = new Animal(); static null: null = null; static undefined: undefined = undefined; > const animal = new Animal() .add(Type.animal) // Ok .add(Type.interface) // Error .add(Type.any) // Ok .add(Type.null) // Ok .add(Type.undefined); // Ok
Не будет лишним упомянуть, что в тех случаях, когда тип не указан явно, а в качестве значения выступает ссылка на экземпляр ( this ), то вывод типов будет указывать на принадлежность к полиморфному типу this .
class Animal public instance = this; // instance: this public getInstance() // getInstance(): this const instance = this; // instance: this return this; > >
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: void;> f2: f2 >; v1.f2(); // Error let v2 = // v2: 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 .