Перейти к основному содержимому

Слияние объявлений в TypeScript

Слияние объявлений — это то, что TypeScript делает, когда находит несколько объявлений с одним и тем же именем: он объединяет их в одно определение.


Как работает слияние объявлений

Слияние объявлений позволяет объединить несколько объявлений с общим именем в одно определение. Так можно строить сложные типы шаг за шагом и расширять существующие типы, не теряя безопасность типов.

Основные преимущества:

  • Постепенное расширение: тип собирается по частям в нескольких объявлениях.
  • Расширяемость: можно добавить члены к существующему типу, не трогая его исходное определение.
  • Организация кода: большой тип можно разделить на логические группы.
  • Совместимость: при необходимости можно расширить определения типов из сторонних библиотек.

Типичные случаи применения:

  • Расширение встроенных типов и типов сторонних библиотек.
  • Добавление информации о типах для библиотек на JavaScript.
  • Организация больших интерфейсов в нескольких файлах.
  • Создание «текучих» API с цепочками вызовов методов.
  • Реализация паттерна расширения модуля (module augmentation).

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

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

Пример

Два объявления Person становятся одним интерфейсом с полями name, age, address и email, поэтому объект должен содержать все четыре.

Prompt
Покажите, что два объявления interface Person в одном файле сливаются в один тип. Первое объявляет name: string и age: number, второе объявляет address: string и email: string. Создайте объект person со всеми четырьмя свойствами (John, 30, "123 Main St", [email protected]) и выведите объект целиком.
Вывод: программа на TypeScript и захваченный вывод

Перегрузки функций и слияние

Можно написать несколько сигнатур функции с одним именем. Они сливаются в одну перегруженную функцию, а все случаи обрабатывает одна реализация.

Пример

У processValue три сигнатуры перегрузки и одна реализация. Перегрузки точно сообщают вызывающему коду, какой тип возвращаемого значения соответствует каждому типу аргумента.

Prompt
Объявите три сигнатуры перегрузки для processValue — одна принимает и возвращает string, другая number, третья boolean — а затем одну реализацию, которая принимает string | number | boolean. Реализация переводит строки в верхний регистр, удваивает числа и инвертирует логические значения. Вызовите её с "hello", 10 и true и выведите каждый результат.
Вывод: программа на TypeScript и захваченный вывод

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

Пространства имён с одинаковым именем тоже сливаются. Экспортированные члены одного блока видны в остальных.

Пример

Второй блок Validation может реализовать StringValidator из первого блока, а внешний код обращается ко всему через единое объединённое пространство имён.

Prompt
Покажите, как два блока namespace Validation сливаются в одно пространство имён. Первый экспортирует интерфейс StringValidator с методом isValid(s: string): boolean; второй экспортирует интерфейс NumberValidator и класс ZipCodeValidator, который реализует StringValidator, проверяя, что строка состоит ровно из пяти цифр. Создайте ZipCodeValidator через Validation.ZipCodeValidator и выведите результат isValid для "12345", "1234" и "abcde".
Вывод: программа на TypeScript и захваченный вывод

Слияние класса и интерфейса

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

Слияние класса с интерфейсом

Интерфейс Cart объявляет calculateTotal(), а класс Cart реализует этот метод вместе со своими собственными членами items и addItem.

script.ts
// Interface declaration
interface Cart {
  calculateTotal(): number;
}

// Class declaration with same name
class Cart {
  items: { name: string; price: number }[] = [];

  addItem(name: string, price: number): void {
    this.items.push({ name, price });
  }

    // Must implement the interface method
    calculateTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}

// Using the merged class and interface
const cart = new Cart();
cart.addItem("Book", 15.99);
cart.addItem("Coffee Mug", 8.99);

console.log(`Total: $${cart.calculateTotal().toFixed(2)}`);

Слияние перечислений

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

Пример

В итоге Direction получает четыре члена. Обратный поиск по значению (Direction[0]) работает по всему объединённому перечислению.

Prompt
Покажите, как два объявления enum Direction сливаются в одно перечисление. Первое объявляет North и South (автоматическая нумерация 0 и 1); второе объявляет East = 2 и West = 3. Выведите числовое значение всех четырёх членов, а затем выведите Direction[0] и Direction[2], чтобы показать обратный поиск по значению.
Вывод: программа на TypeScript и захваченный вывод

Расширение модуля

Существующий модуль или библиотеку можно расширить, объявив дополнительные типы и функции с тем же именем. Это называется расширением модуля (module augmentation).

Пример

Первый блок declare namespace LibraryModule изображает стороннюю библиотеку. Второй блок добавляет новый интерфейс, новое необязательное свойство в User и новую функцию. Так как блоки declare не содержат кода времени выполнения, в примере к globalThis привязывается небольшая заглушка-реализация, чтобы файл можно было запустить.

Prompt
Покажите расширение модуля с помощью двух блоков declare namespace LibraryModule: первый (изображающий стороннюю библиотеку) объявляет интерфейс User (id, name) и функцию getUser(id: number): User; второй добавляет интерфейс UserPreferences (theme, notifications), добавляет необязательное свойство preferences?: UserPreferences в User и объявляет getUserPreferences(userId: number): UserPreferences. Так как блоки declare не содержат кода времени выполнения, привяжите к globalThis небольшую заглушку-реализацию LibraryModule, чтобы файл можно было запустить. Вызовите getUser(123) и выведите user.preferences?.theme, затем вызовите getUserPreferences(123) и выведите prefs.notifications.
Вывод: программа на TypeScript и захваченный вывод

Лучшие практики

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

  • Порядок важен для перегрузок функций: сигнатура реализации должна быть самой общей.
  • Нефункциональные члены должны быть совместимы: если два интерфейса объявляют свойство с одинаковым именем, типы должны быть одинаковыми или совместимыми.
  • Более поздние интерфейсы имеют приоритет: если объединяемые интерфейсы конфликтуют, побеждает последнее объявление.
  • Приватные и защищённые члены: классы не могут слиться, если у них есть private- или protected-члены с одинаковым именем, но разными типами.
  • Экспорт из пространства имён: после слияния снаружи видны только экспортированные объявления.

Вопросы производительности

  • Время компиляции: активное использование слияния объявлений может замедлить компиляцию.
  • Проверка типов: сложные объединённые типы могут замедлить работу редактора.
  • Размер сборки: слияние объявлений никак не влияет на производительность во время выполнения и на размер сборки.

Советы по оптимизации:

  • Держите объединяемые интерфейсы сфокусированными и связными.
  • Избегайте глубокой вложенности в объединённых типах.
  • Для простых комбинаций типов используйте псевдонимы type вместо слияния.