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 () { // ... }
Глобальные дополнения ведут себя так же, как и дополнения модулей, и имеют такие же ограничения.
Поддержите перевод документации:
Documentation generated by mdoc.