Table of Contents #

Введение #

Для описания JavaScript-объектов на уровне типов в TypeScript используется несколько уникальных принципов. Один из примеров подобного совершенно исключительного для TypeScript принципа — 'слияние объявлений'. Понимание работы данного механизма дает преимущество при работе с уже существующим JavaScript-кодом, а также открывает дверь к более сложным принципам абстракции.

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

Основные понятия #

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

Вид объявления Пространство имен Тип Значение
Пространство имен X X
Класс X X
Перечисление X X
Интерфейс X
Псевдоним типа X
Функция X
Переменная X




Понимание того, что создается тем или иным объявлением, помогает разобраться, как происходит слияние.

Слияние интерфейсов #

Самый простой и, возможно, наиболее часто используемый вид слияния — слияние интерфейсов. На самом простом уровне такое слияние механически объединяет члены обоих объявлений в один интерфейс с тем же именем.

interface Box {
    height: number;
    width: number;
}

interface Box {
    scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10};

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

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

Таким образом, в данном примере:

interface Cloner {
    clone(animal: Animal): Animal;
}

interface Cloner {
    clone(animal: Sheep): Sheep;
}

interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
}

три интерфейса будут слиты вместе и получится следующее объявление:

interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
    clone(animal: Sheep): Sheep;
    clone(animal: Animal): Animal;
}

Обратите внимание, что элементы внутри групп сохраняют свой порядок, однако сами группы упорядочены так, что более поздние перегрузки находятся в начале.

Единственное исключение из этого правила — специализированные сигнатуры. Если у сигнатуры есть параметр с типом одиночного строкового литерала (т. е., не объединение строковых литералов, например), то она поднимется к верху объединенного списка перегрузок.

К примеру, следующие интерфейсы будут слиты друг с другом:

interface Document {
    createElement(tagName: any): Element;
}
interface Document {
    createElement(tagName: "div"): HTMLDivElement;
    createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
    createElement(tagName: string): HTMLElement;
    createElement(tagName: "canvas"): HTMLCanvasElement;
}

И результирующее объявлениеDocument будет следующим:

interface Document {
    createElement(tagName: "canvas"): HTMLCanvasElement;
    createElement(tagName: "div"): HTMLDivElement;
    createElement(tagName: "span"): HTMLSpanElement;
    createElement(tagName: string): HTMLElement;
    createElement(tagName: any): Element;
}

Слияние пространств имен #

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

Для слияния пространств имен объявления типов из экспортируемых интерфейсов в каждом из пространств имен объединяются, и образуется единое пространство имен с объединенными определениями интерфейсов внутри.

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

В данном примере объединенное объявление Animals

namespace Animals {
    export class Zebra { }
}

namespace Animals {
    export interface Legged { numberOfLegs: number; }
    export class Dog { }
}

эквивалентно:

namespace Animals {
    export interface Legged { numberOfLegs: number; }

    export class Zebra { }
    export class Dog { }
}

Такая модель слияния пространств имен неплоха для начала, но необходимо понять, что происходит с членами, которые не экспортируются. Неэкспортируемые члены видны только в оригинальном (не объединенном) пространстве имен. Это значит, что после слияния они не будут видны членам из других объявлений.

Более ясно это видно на следующем примере:

namespace Animal {
    let haveMuscles = true;

    export function animalsHaveMuscles() {
        return haveMuscles;
    }
}

namespace Animal {
    export function doAnimalsHaveMuscles() {
        return haveMuscles;  // <-- ошибка, haveMuscles здесь не видна
    }
}

Поскольку haveMuscles не экспортируется, она видна только в функции animalsHaveMuscles из того же необъединенного пространства имен. Функция doAnimalsHaveMuscles, хотя и входит в объединенное пространство имен Animal, не видит неэкспортируемый член.

Слияние пространств имен с классами, функциями и перечислениями #

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

Слияние пространств имен с классами

Это позволяет описывать вложенные классы.

class Album {
    label: Album.AlbumLabel;
}
namespace Album {
    export class AlbumLabel { }
}

Правила видимости для объединяемых членов такие же, как те, что описаны в разделе 'Слияние пространств имен', поэтому AlbumClass должен быть экспортирован, чтобы он был виден в объединенном классе. Итоговый результат — класс, используемый изнутри другого класса. Кроме того, пространства имен можно использовать для добавления статических членов к существующим классам.

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

function buildLabel(name: string): string {
    return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
    export let suffix = "";
    export let prefix = "Hello, ";
}

alert(buildLabel("Sam Smith"));

Похожим образом пространства имен можно использовать для расширения перечислений статическими членами:

enum Color {
    red = 1,
    green = 2,
    blue = 4
}

namespace Color {
    export function mixColor(colorName: string) {
        if (colorName == "yellow") {
            return Color.red + Color.green;
        }
        else if (colorName == "white") {
            return Color.red + Color.green + Color.blue;
        }
        else if (colorName == "magenta") {
            return Color.red + Color.blue;
        }
        else if (colorName == "cyan") {
            return Color.green + Color.blue;
        }
    }
}

Запрещенные слияния #

Не все слияния допустимы. На данный момент классы не могут объединяться с другими классами или с переменными. Для информации о том, как можно эмулировать слияние классов, см. раздел Примеси в TypeScript.

Дополнения модулей #

Хотя JavaScript-модули не поддерживают слияние, уже существующие объекты можно модифицировать, импортируя и затем изменяя их. Посмотрим на пример реализации шаблона проектирования 'Наблюдатель' (Observable):

// observable.js
export class Observable {
    // ... реализация оставлена в качестве упражнения для читателя ...
}

// map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
    // ... еще одно упражнение для читателя
}

Это отлично работает и в TypeScript, но компилятор ничего не знает о Observable.prototype.map. Чтобы рассказать компилятору об этом свойстве, можно использовать дополнение модуля:

// observable.ts остается тем же
// map.ts
import { Observable } from "./observable";
declare module "./observable" {
    interface Observable {
        map <U>(f: (x: T) => U): Observable <U>;
    }
}
Observable.prototype.map = function (f) {
    // ... еще одно упражнение для читателя
}


// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable;
o.map(x => x.toFixed());

Имя модуля разрешается так же, как и спецификаторы модуля в import/export. См. Модули для большей информации. После этого объявления в дополнении сливаются, словно бы они определены в том же модуле, что и исходный. Создавать новое определение верхнего уровня в дополнении нельзя — только модификации для уже существующих определений.

Глобальное дополнение

Кроме того, из модуля можно добавлять объявления в глобальную область видимости:

// observable.ts
export class Observable {
    // ... все еще не реализовано ...
}

declare global {
    interface Array {
        toObservable(): Observable;
    }
}

Array.prototype.toObservable = function () {
    // ...
}

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

Источник







Поддержите перевод документации:



Поддерживатель | Github Репозиторий


Documentation generated by mdoc.
Молния! Обновления, новости и статьи Typescript.