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

Слияние объявлений — это мощная возможность TypeScript, позволяющая объединять несколько объявлений с одинаковым именем в одно определение.

Это позволяет поэтапно выстраивать сложные типы и безопасно расширять существующие типы с сохранением типобезопасности.


Ключевые преимущества

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

Распространённые сценарии использования

  • расширение встроенных типов и типов из сторонних библиотек;
  • добавление типовой информации для JavaScript‑библиотек;
  • организация крупных интерфейсов в нескольких файлах;
  • создание плавных API с цепочкой вызовов методов;
  • реализация шаблона дополнения модулей.

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

Интерфейсы с одинаковым именем объединяются автоматически.

Пример


// Первое объявление
interface Person {
  name: string;
  age: number;
}

// Второе объявление с тем же именем
interface Person {
  address: string;
  email: string;
}

// TypeScript объединяет их в:
// interface Person {
//   name: string;
//   age: number;
//   address: string;
//   email: string;
// }

const person: Person = {
  name: "John",
  age: 30,
  address: "123 Main St",
  email: "john@example.com"
};

console.log(person);


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

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

Пример


// Перегрузки функции
function processValue(value: string): string;
function processValue(value: number): number;
function processValue(value: boolean): boolean;

// Реализация, обрабатывающая все перегрузки
function processValue(value: string | number | boolean): string | number | boolean {
  if (typeof value === "string") {
    return value.toUpperCase();
  } else if (typeof value === "number") {
    return value * 2;
  } else {
    return !value;
  }
}

// Использование функции с разными типами
console.log(processValue("hello")); // "HELLO"
console.log(processValue(10)); // 20
console.log(processValue(true)); // false


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

Пространства имён с одинаковым именем объединяются.

Пример


namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean;
  }
}

namespace Validation {
  export interface NumberValidator {
    isValid(n: number): boolean;
  }

  export class ZipCodeValidator implements StringValidator {
    isValid(s: string): boolean {
      return s.length === 5 && /^\d+$/.test(s);
    }
  }
}

// После слияния:
// namespace Validation {
//   export interface StringValidator { isValid(s: string): boolean; }
//   export interface NumberValidator { isValid(n: number): boolean; }
//   export class ZipCodeValidator implements StringValidator { ... }
// }

// Использование объединённого пространства имён
const zipValidator = new Validation.ZipCodeValidator();

console.log(zipValidator.isValid("12345")); // true
console.log(zipValidator.isValid("1234")); // false
console.log(zipValidator.isValid("abcde")); // false


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

Объявление класса может объединяться с интерфейсом того же имени.

Пример


// Объявление интерфейса
interface Cart {
  calculateTotal(): number;
}

// Объявление класса с тем же именем
class Cart {
  items: { name: string; price: number }[] = [];

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

  // Должно реализовывать метод интерфейса
  calculateTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}

// Использование объединённых класса и интерфейса
const cart = new Cart();
cart.addItem("Book", 15.99);
cart.addItem("Coffee Mug", 8.99);

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


Слияние перечислений (enum)

Объявления перечислений с одинаковым именем объединяются.

Пример


// Первая часть перечисления
enum Direction {
  North,
  South
}

// Вторая часть перечисления
enum Direction {
  East = 2,
  West = 3
}

// После слияния:
// enum Direction {
//   North = 0,
//   South = 1,
//   East = 2,
//   West = 3
// }

console.log(Direction.North); // 0
console.log(Direction.South); // 1
console.log(Direction.East); // 2
console.log(Direction.West); // 3

// Также можно обращаться по значению
console.log(Direction[0]); // "North"
console.log(Direction[2]); // "East"


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

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

Пример


// Исходное определение библиотеки
// Предположим, это из сторонней библиотеки
declare namespace LibraryModule {
  export interface User {
    id: number;
    name: string;
  }
  export function getUser(id: number): User;
}

// Расширение дополнительной функциональностью (ваш код)
declare namespace LibraryModule {
  // Добавить новый интерфейс
  export interface UserPreferences {
    theme: string;
    notifications: boolean;
  }

  // Добавить новое свойство в существующий интерфейс
  export interface User {
    preferences?: UserPreferences;
  }

  // Добавить новую функцию
  export function getUserPreferences(userId: number): UserPreferences;
}

// Использование дополненного модуля
const user = LibraryModule.getUser(123);
console.log(user.preferences?.theme);

const prefs = LibraryModule.getUserPreferences(123);
console.log(prefs.notifications);


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

При использовании слияния объявлений следует учитывать несколько правил:

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

Соображения по производительности

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

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

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