interface Point {
readonly x: number;
readonly y: number;
}
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // ошибка!
ro.push(5); // ошибка!
ro.length = 100; // ошибка!
a = ro; // ошибка!В последней строке примера можно видеть, что даже присваивание ReadonlyArray обычному массиву недопустимо.
Вы можете делать свойства доступными только для чтения с помощью ключевого слова readonly.
Свойства, доступные только для чтения, должны быть инициализированы при их объявлении или в конструкторе.
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // ошибка! name is readonly.Доработка:
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {}
}Обратите внимание на то, как мы убрали theName и сократили параметр конструктора readonly name: string, чтобы создать и инициализировать член name. Мы объединили объявление и присваивание в одном месте.
Свойства параметров объявляются перед параметром конструктора, у которого есть модификатор доступности,
readonly или и то, и другое. Использование свойства параметра private объявляет и инициализирует приватный член;
то же самое делают public, protected и readonly.
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}Интерфейсы описывают публичную часть класса, но не приватную. Это не дает возможности указывать с помощью интефейса, что класс должен использовать конкретные типы для своих приватных членов.
Разница между статической частью и экземпляром класса. Работая с классами и интерфейсами, полезно помнить, что класс имеет два типа: тип статической части и тип экземпляра. Вы могли столкнуться с ошибкой, если создавали интерфейс с конструктором, а потом пытались написать класс, который реализовывал бы его:
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}Так происходит из-за того, что, когда класс реализует интерфейс, происходит проверка типа только его экземпляра. Конструктор же находится в статической части, и не включается в эту проверку.
Вместо такого подхода нужно работать напрямую со статической частью класса. В следующем примере мы определяем
два интерфейса: ClockConstructor для конструктора, и ClockInterface для экземпляра класса. Затем, для удобства,
мы определяем функцию-конструктор createClock, которая создает объекты того типа, который передается ей
в качестве аргумента.
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);Так как первый параметр createClock имеет тип ClockConstructor, то в createClock(AnalogClock, 7, 32)
происходит проверка на то, что AnalogClock имеет подходяющую сигнатуру конструктора.
Интерфейсы, расширяющие классы.
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control {
select() { }
}
class TextBox extends Control {
select() { }
}
class Image extends Control {}
class Location {
select() { }
}В этом примере SelectableControl содержит все члены класса Control, включая приватное свойство state.
Так как state — приватный член, реализовать интерфейс SelectableControl смогут только наследники Control.
Так будет потому, что для совместимости приватных членов необходимо, чтобы они были объявлены в одном и
том же базовом классе, а это возможно лишь для наследников Control.
Внутри кода Control можно получить доступ к приватному члену state через экземпляр SelectableControl.
По сути, SelectableControl ведет себя так же, как Control, о котором известно, что у него есть метод select.
Классы Button и TextBox — подтипы SelectableControl (так как оба унаследованы от Control и у них есть метод select),
однако Image и Location таковыми не являются.
Конструктор тоже может иметь модификатор protected. Это означает, что класс не может быть создан за
пределами содержащего его класса, но может быть наследован. Например:
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
// Employee can extend Person
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // ошибка: The 'Person' constructor is protectedabstract class Department {
constructor(public name: string) {}
printName(): void {
console.log("Department name: " + this.name);
}
abstract printMeeting(): void; // должен быть реализован в производном классе
}
class AccountingDepartment extends Department {
constructor() {
super("Accounting and Auditing"); // конструкторы в производных классах должны вызывать super()
}
printMeeting(): void {
console.log("The Accounting Department meets each Monday at 10am.");
}
generateReports(): void {
console.log("Generating accounting reports...");
}
}
let department: Department; // окей, создана ссылка на абстрактный класс
department = new Department(); // ошибка: cannot create an instance of an abstract class
department = new AccountingDepartment(); // окей, создан и присвоен не абстрактный класс
department.printName();
department.printMeeting();
department.generateReports(); // ошибка: method doesn't exist on declared abstract typeinterface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// ВНИМАНИЕ: Сейчас функция явно указывает на то, что она должна вызываться на объекте типа Deck
createCardPicker: function(this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);Теперь компилятор знает, что функция createCardPicker ожидает, что будет вызвана на объекте с типом Deck.
Это значит, что тип значения this теперь — Deck, а не any, и флаг --noImplicitThis не будет выдавать ошибок.
Также можно столкнуться с ошибками, связанными с this в функциях обратного вызова, когда функции передаются
в библиотеку, которая позже будет их вызывать. Поскольку переданная функция будет вызвана библиотекой
как обычная функция, у this будет значение undefined. Приложив некоторые усилия, можно использовать параметр this,
чтобы предотвратить подобные ошибки. Во-первых, разработчик библиотеки должен сопроводить тип функции
обратного вызова параметром this:
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}this: void означает, что addClickListener предполагает, что функция onclick не требует this.
Во-вторых, код, который вызывается, нужно также сопроводить параметром this:
class Handler {
info: string;
onClickBad(this: Handler, e: Event) {
// Тут используется this! Эта функция упадет во время выполнения!
this.info = e.message;
};
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!Когда this указан, это явно отражает тот факт, что onClickBad должна вызываться на экземпляре класса Handler.
Теперь TypeScript обнаружит, что addClickListener требует функцию с this: void. Чтобы исправить эту ошибку,
изменим тип this:
class Handler {
info: string;
onClickGood(this: void, e: Event) {
// здесь нельзя использовать переменную this, потому что у нее тип void!
console.log('clicked!');
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);Так как в функции onClickGood указано, что тип this — void, ее можно передать в addClickListener.
Конечно, это означает и то, что теперь в ней нельзя использовать this.info. Но если нужно и то, и другое,
то придется использовать стрелочную функцию:
class Handler {
info: string;
onClickGood = (e: Event) => { this.info = e.message }
}Это будет работать, поскольку стрелочные функции не захватывают this из контекста, в котором выполняются,
и их можно свободно передавать там, где ожидается функция с this: void. Недостаток такого решения в том,
что для каждого объекта Handler будет создаваться своя стрелочная функция. Методы же, напротив, создаются
только однажды, ассоциируются с прототипом Handler, и являются общими для всех объектов этого класса.
Функция может принимать разного типа аргумент и на основании его типа - обрабатывать нужное поведение.
function pickCard(x): any {
// Работаем с объектом/массивом?
// Значит, нам передали колоду и мы выбираем карту
if (typeof x == "object") {
...
}
// Иначе даем возможность выбрать карту
else if (typeof x == "number") {
...
}
}Но как описать такое поведение с помощью системы типов? Нужно указать для одной функции несколько типов, создав список перегрузок. Этот список компилятор будет использовать для проверок при вызове функции.
function pickCard(x: {suit: string; card: number;}[]): number;
function pickCard(x: number): {suit: string; card: number;};
function pickCard(x): any {
// Работаем с объектом/массивом?
// Значит, нам передали колоду и нужно выбрать карту
if (typeof x == "object") {
...
}
// Иначе даем возможность выбрать карту
else if (typeof x == "number") {
...
}
}В этом списке перегрузок всего два элемента, один из которых принимает object, а другой — number.
Вызов pickCard с параметрами любых других типов приведет к ошибке.
В примере с loggingIdentity требуется получить доступ к свойству length объекта arg, но компилятор
не может быть уверен, что у любого типа будет такое свойство, поэтому предупреждает об этом.
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Ошибка: у T нет свойства length.
return arg;
}Решается это так:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Теперь мы знаем, что у объекта есть свойство length, поэтому, ошибки нет.
return arg;
}Если, к примеру, нужно принять два объекта и копировать свойства из одного в другой: Нужно удостовериться, что мы случайно не добавим какое-либо лишнее свойство, поэтому добавим ограничение между двумя типами:
function copyFields<T extends U>(target: T, source: U): T {
for (let id in source) {
target[id] = source[id];
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 }); // Все в порядке.
copyFields(x, { Q: 90 }); // Ошибка: у 'x' нет свойства 'Q'.Реализуя паттерн "фабрика" с использованием обобщений, необходимо указывать на тип класса с помощью его функции-конструктора. К примеру:
function create<T>(c: {new(): T; }): T {
return new c();
}Более сложный пример использует свойство прототипа, чтобы вывести и ограничить отношения между конструктором и типом экземпляра класса.
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Lion extends Animal {
keeper: ZooKeeper;
}
function findKeeper<A extends Animal, K> (
a: {
new(): A;
prototype: {keeper: K};
}
): K {
return a.prototype.keeper;
}
findKeeper(Lion).nametag; // проверка типов!// Здесь Rhino — носорог, Elephant — слон, а Snake — змея
let zoo = [new Rhino(), new Elephant(), new Snake()];В идеале, хотелось бы, чтобы тип zoo был выведен как Animal[] (то есть массив объектов класса Animal — животное).
Но, так как в массиве нет ни одного объекта, который бы имел класс именно Animal, компилятор не способен получить
такой результат. Чтобы исправить это, нужно явно указать тип, если ни один объект не имеет тип, базовый для всех остальных:
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];Если компилятору не удается найти наилучший общий тип, результатом выведения будет тип пустого объекта, то есть {}.
Так как у такого типа нет членов, попытка использовать какие-либо его свойства приведет к ошибке.
Совместимость типов в TypeScript основывается на структурной типизации. Структурная типизация — это способ выявления отношений типов на основании исключительно состава их членов. Этот подход отличается от номинативной типизации. Посмотрим на следующий код:
interface Named {
name: string;
}
class Person {
name: string;
}
let p: Named;
// Все подходит, поскольку используется структурная система типов
p = new Person();Структурная система типов TypeScript была спроектирована с учетом того, как обычно пишется код на JavaScript. Поскольку в JavaScript широко используются анонимные объекты, такие как функциональные выражения и литералы объектов, гораздо более естественно будет описывать их отношения с помощью структурной системы, а не номинативной.
Замечание относительно надежности.
Основное правило системы типов TypeScript таково — x совместимо с y, если y имеет по крайней мере те же самые члены,
что и x. К примеру:
interface Named {
name: string;
}
let x: Named;
// выведенный для y тип — { name: string; location: string; }
let y = { name: "Alice", location: "Seattle" };
x = y;Чтобы понять, может ли y быть присвоена x, компилятор для каждого из свойств x ищет соответствующее совместимое
свойство в y. В данном случае переменная y должна иметь свойство под именем name строкового типа.
Оно есть, и присваивание допускается.
То же самое правило используется в случае проверки аргументов при вызове функции:
function greet(n: Named) {
alert("Привет, " + n.name);
}
greet(y); // ОКОбратите внимание, что y обладает дополнительным свойством location, но это не приводит к ошибке.
При проверке на совместимость учитываются только члены целевого типа (в данном случае это Named).
Сравнение двух функций.
Сравнение типов двух примитивов или объектов происходит относительно просто, однако вопрос о том, какие функции должны считаться совместимыми, немного более сложен. Начнем с простого примера с двумя функциями, отличающимися только списками параметров:
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // Все нормально
x = y; // ОшибкаЧтобы проверить, допустимо ли присваивание x к y, сначала просматривается список параметров.
Для каждого параметра функции x у функции y должен быть соответствующий параметр совместимого типа.
Имена параметров не принимаются во внимание — важны лишь типы. В данном случае для каждого параметра x есть
соответствующий совместимый параметр в функции y, поэтому присваивание допускается.
Второе присваивание приводит к ошибке, поскольку y имеет обязательный второй параметр, которого нет у x,
и операция не допускается.
Может показаться интересным, почему разрешается "терять" параметры функции, как это происходит при y = x.
Причина этому то, что игнорирование лишних параметров функции — довольно частая практика в JavaScript.
К примеру, Array#forEach передает функции обратного вызова три параметра: элемент массива, его индекс, и массив,
в котором тот содержится. Несмотря на это, очень удобно работать с функцией обратного вызова, которая использует
лишь первый параметр:
let items = [1, 2, 3];
// Не заставлять использовать дополнительные параметры
items.forEach((item, index, array) => console.log(item));
// Все должно работать!
items.forEach(item => console.log(item));Теперь посмотрим, как обрабатываются типы возвращаемых значений. Для этого используем две функции, отличающиеся только типами возвращаемых значений:
let x = () => ({name: "Alice"});
let y = () => ({name: "Alice", location: "Seattle"});
x = y; // Работает
y = x; // Ошибка, поскольку у x() нет свойства locationНеобходимо, чтобы тип возвращаемого значения исходной функции был подтипом типа возвращаемого значения целевой функции.
Классы.
Классы работают подобно типам объектных литералов и интерфейсам, но с одним исключением: у них есть тип статической части и тип экземпляра. При сравнении двух объектов, которые имеют классовый тип, сравниваются только члены экземпляра. Статические члены и конструкторы не влияют на совместимость.
class Animal {
feet: number;
constructor(name: string, numFeet: number) { }
}
class Size {
feet: number;
constructor(numFeet: number) { }
}
let a: Animal;
let s: Size;
a = s; // OK
s = a; // OKПриватные и защищенные члены классов влияют на их совместимость. Когда экземпляр класса проходит проверку на совместимость, то, если у него есть приватный член, у целевого типа тоже должен быть приватный член, объявленный в том же классе. Это относится и к экземплярам с защищенными членами. Такая особенность позволяет классам быть совместимыми при присваивании базовому классу, но не классу из другой иерархии наследования, даже если бы он имел такую же форму.
Если у нас есть значение типа объединения, мы можем получить доступ только к тем его элементам, которые являются общими для всех типов в объединении.
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // ок
pet.swim(); // ошибкаЗащитники типа и различие типов. (Type guards)
Как мы уже упоминали, Вы можете получить доступ только к элементам, которые гарантированно будут во всех составляющих типа объединения.
let pet = getSmallPet();
// Каждый доступ к свойству приведёт к ошибке
if (pet.swim) {
pet.swim();
} else if (pet.fly) {
pet.fly();
}Чтобы этот код работал, нам нужно воспользоваться утверждением типа (type assertion).
let pet = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
} else {
(<Bird>pet).fly();
}Чтобы определить защитник типа, нужно просто определить функцию, чей тип возврата является предикатом типа:
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}pet is Fish - это предикат типа. Предикат принимает форму parameterName is Type, где parameterName -
имя параметра из текущей сигнатуры функции.
Всякий раз, когда с некоторой переменной вызывается isFish, TypeScript ограничивает эту переменную в специфический тип,
при условии, что оригинальный тип совместим.
// Оба вызова, 'swim' и 'fly', теперь в порядке.
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}Защитники типа typeof.
Вместо определения примитива:
function isNumber(x: any): x is number {
return typeof x === "number";
}...можно использовать оператор typeof:
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Ожидал строку или число, а получил '${padding}'.`);
}
let indentedString = padLeft("Hello world", true); // Ошибка компиляции.Защитники типа instanceof.
Защитники типа instanceof - это способ ограничения типов используя их функцию-конструктор. Например, давайте возьмём наш ранее введенный пример:
interface Padder {
getPaddingString(): string
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) { }
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) { }
getPaddingString() {
return this.value;
}
}
function getRandomPadder() {
return Math.random() < 0.5 ?
new SpaceRepeatingPadder(4) :
new StringPadder(" ");
}
// Тип - 'SpaceRepeatingPadder | StringPadder'
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
padder; // тип ограничен к 'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
padder; // тип ограничен к 'StringPadder'
}Псевдонимы.
namespace Shapes {
export namespace Polygons {
export class Triangle { }
export class Square { }
}
}
import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // То же, что и 'new Shapes.Polygons.Square()';Композиция Декоратора.
При вычислении нескольких декораторов на одном объявлении, выполняются следующие шаги:
- Выражения для каждого декоратора вычисляются сверху вниз;
- Результаты затем вызываются как функции снизу вверх;
function f() {
console.log("f(): вычисляется");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): вызван");
}
}
function g() {
console.log("g(): вычисляется");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("g(): вызван");
}
}
class C {
@f()
@g()
method() {}
}Что выведет в консоль:
f(): вычисляется
g(): вычисляется
g(): вызван
f(): вызван
Вычисление Декоратора.
Есть определённый порядок применения декораторов к различным объявлениям внутри класса:
- Декораторы Параметров с последующими Декораторами Методов, Акцессоров или Свойств применяются для каждого члена экземпляра.
- Декораторы Параметров с последующими Декораторами Методов, Акцессоров или Свойств применяются для каждого статического члена.
- Декораторы Параметров применяются для конструкторов.
- Декораторы Класса применяются для класса.
Декораторы Класса.
Декоратор Класса объявляется перед объявлением класса.
Декоратор Класса применяется к конструктору класса и может быть использован для наблюдения, модифицирования или
замещения определения класса.
Декоратор Класса не может быть использован в файле объявлений или в любом другом окружающем контексте (например, в классе declare).
Выражение декоратора класса будет вызываться как функция во время исполнения, а конструктор отдекарированного класса - как его единственный аргумент.
Если декоратор класса возвратит значение, он заменит объявление класса с помощью предоставленного конструктора.
ЗАМЕТКА Если Вы решите вернуться к новому конструктору, тогда Вы должны позаботиться о сохранении исходного прототипа. Логика, использующая декораторы во время выполнения программы, не сделает это за Вас.
Пример декоратора класса:
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Привет, " + this.greeting;
}
}Мы можем определить декоратор @sealed, используя следующее объявление функции:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}Когда декоратор @sealed будет выполнен, он 'запечатает' (seal - печать) конструктор, и его прототип.
Декораторы Методов.
Декоратор Метода объявляется перед объявлением метода
Декоратор применяется к Дескриптору Свойства метода, также может быть использован для наблюдения,
модифицирования или замещения определения метода.
Декоратор метода не может быть использован в файле объявления, при перегрузке или в любом другом окружающем
контексте (например, в классе declare).
Выражение для декоратора метода будет вызываться как функция во время исполнения программы со следующими тремя аргументами:
- Либо конструктор класса для статичного члена, либо прототип класса для члена экземпляра.
- Имя члена.
- Дескриптор Свойства члена.
ЗАМЕТКА Дескриптор Свойства будет
undefined, если цель Вашего скрипта меньше, чем стандарт ES5.
Если декоратор метода возвратит значение, это значение будет использоваться как Дескриптор Свойства для метода.
ЗАМЕТКА Возвращаемое значение игнорируется, если цель вашего скрипта меньше, чем стандарт ES5.
Пример декоратора метода (@enumerable), применённый к методу класса Greeter:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Привет, " + this.greeting;
}
}Мы можем определить декоратор @enumerable, используя следующее объявление функции:
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}Декоратор @enumerable(false) - это фабричный декоратор.
Когда будет вызван декоратор @enumerable(false), он изменит свойство enumerable дескриптора свойства.
Декоратор Акцессора.
Декоратор Акцессора объявляется перед объявлением акцессора.
Декоратор акцессора применяется к Дескриптору Свойства и может быть использован для наблюдения
модифицирования или замещения определения акцессора.
Декоратор акцессора не может быть использован в файле объявления или в любом другом окружающем
контексте (например, класс declare).
ЗАМЕТКА TypeScript запрещает декорирование
getиsetакцессоров для одного члена. Вместо этого, все декораторы члена должны быть применены к первому акцессору, указанному в текстовом порядке. Это потому, что декораторы применяются к Дескриптору Свойства, который комбинирует оба метода доступаgetиset, а не каждое объявление в отдельности.
Выражение для декоратора акцессора будет вызвано как функция во время исполнения со следующими тремя аргументами:
- Либо конструктор класса для статичного члена, либо прототип класса для члена экземпляра.
- Имя члена.
- Дескриптор Свойства для члена.
ЗАМЕТКА Дескриптор Свойства будет
undefined, если цель вашего скрипта меньше, чем стандарт ES5.
Если декоратор акцессора возвратит значение, это значение будет использоваться как Дексриптор Свойства для члена.
ЗАМЕТКА Возвращаемое значение игнорируется, если цель вашего скрипта меньше, чем стандарт ES5.
Пример декоратора акцессора (@configurable), применённый к члену класса Point:
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() { return this._x; }
@configurable(false)
get y() { return this._y; }
}Мы можем определить декоратор @configurable, используя следующее объявление функции:
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}Декораторы Свойства.
Декоратор Свойства объявляется перед объявлением свойства. Декоратор свойства не может быть использован в файле
объявления или в любом другом окружающем контексте (например, в классе declare).
Выражение декоратора свойства будет вызвано как функция во время исполнения со следующими аргументами:
- Либо конструктор класса для статичного члена, либо прототип класса для члена экземпляра.
- Имя члена.
ЗАМЕТКА Дескриптор Свойства не предоставляется в качестве аргумента для декоратора свойства из-за того, как декораторы свойства инициализируются в TypeScript. Это потому, что на текущий момент не существует механизма описать свойство экзмепляра при определении членов прототипа, также нет способа наблюдения или модифицирования инициализатора для свойства. Так, дескриптор свойства может быть использован только для наблюдения того, что для класса было объявлено свойство.
Если декоратор свойства возвращает значение, это значение будет использоваться в качестве Дескриптора Свойства для члена.
ЗАМЕТКА Возвращаемое значение игнорируется, если цель вашего скрипта меньше, чем стандарт ES5.
Мы можем использовать эту информцию для записи метаинформации о свойстве, как в следующем примере:
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}Мы можем затем определить декоратор @format и getFormat, используя следующие объявления функции:
import "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}Декоратор @format("Привет, %s") фабричный декоратор. При вызове @format("Привет, %s"), декоратор добавляет метаданные
для свойства используя функцию Reflect.metadata из библиотеки reflect-metadata. При вызове getFormat, функция считывает
значение метаданных.
Декораторы Параметров.
Декоратор Параметра объявляется перед объявлением параметра. Декоратор параметра применяется к объявлению
конструктора или методу класса. Декоратор параметра не может быть использован в файле объявлений, при перегрузке
или в любом другом окружающем контексте (например, в классе declare).
Выражение декоратора параметра будет вызвано как функция во время исполнения, со следующими тремя аргументами:
- Либо конструктор класса для статичного члена, либо прототип класса для члена экземпляра.
- Имя члена.
- Порядковый индекс параметра в списке параметров функции.
ЗАМЕТКА Декоратор параметра может быть использован только для наблюдения того, что параметр был объявлен у метода.
Возвращаемое значение декоратора параметра игнорируется.
Пример декоратора параметра (@required), применённый к параметру члена класса Greeter:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Привет " + name + ", " + this.greeting;
}
}Мы затем можем определить декораторы @required и @validate, используя следующие объявления функций:
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<function>) {
let method = descriptor.value;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
throw new Error("Отсутствуют требуемые аргументы.");
}
}
}
return method.apply(this, arguments);
}
}Декоратор @required добавляет метаинформацию, которая помечает параметр как обязательный.
Декоратор @validate затем оборачивает существующий метод greet в функцию, которая проводит проверку аргументов
перед вызовом исходного метода.
// Disposable (одноразовый) mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}
}
// Activatable (активируемый) mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
class SmartObject implements Disposable, Activatable {
constructor() {
setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
}
interact() {
this.activate();
}
// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable]);
let smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);
////////////////////////////////////////
// Где-то в вашей динамической библиотеке
////////////////////////////////////////
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}Код начинается с определения двух классов, которые и будут задействованы в качестве примесей. Каждый из них нацелен на демонстрацию определенной активности или возможности. Позже мы их смешаем, чтобы сформировать новый объединяющий их свойства класс.
Далее мы создадим новый класс, который будет объединять обе примеси.
Первое, на что вы могли обратить внимание, - вместо extends используется implements.
Такой подход позволяет рассматривать классы как интерфейсы и использовать только типы Disposable и Activatable,
а не их реализации. Получается, что реализацию мы должны будем создать в новом классе.
Но проблема в том, что именно этого мы бы и хотели избежать при использовании примесей.
Чтобы не делать реализации заново, мы создаем свойства-дублёры (stand-in properties), типы которых будут получены из соответствующих примесей. Компилятору достаточно, чтобы эти элементы были доступны динамически. Такой подход позволяет нам пользоваться преимуществами примесей, но при условии дополнительной нагрузки по учёту подобных нюансов.
В итоге мы соединяем наши примеси в классе, создавая полную реализацию:
applyMixins(SmartObject, [Disposable, Activatable]);