Слияние объявлений в TypeScript
Слияние объявлений — это то, что TypeScript делает, когда находит несколько объявлений с одним и тем же именем: он объединяет их в одно определение.
Как работает слияние объявлений
Слияние объявлений позволяет объединить несколько объявлений с общим именем в одно определение. Так можно строить сложные типы шаг за шагом и расширять существующие типы, не теряя безопасность типов.
Основные преимущества:
- Постепенное расширение: тип собирается по частям в нескольких объявлениях.
- Расширяемость: можно добавить члены к существующему типу, не трогая его исходное определение.
- Организация кода: большой тип можно разделить на логические группы.
- Совместимость: при необходимости можно расширить определения типов из сторонних библиотек.
Типичные случаи применения:
- Расширение встроенных типов и типов сторонних библиотек.
- Добавление информации о типах для библиотек на JavaScript.
- Организация больших интерфейсов в нескольких файлах.
- Создание «текучих» API с цепочками вызовов методов.
- Реализация паттерна расширения модуля (module augmentation).
Слияние интерфейсов
Интерфейсы с одинаковым именем сливаются автоматически. В результате получается интерфейс со всеми членами из всех объявлений.
Пример
Два объявления Person становятся одним интерфейсом с полями name, age, address и email, поэтому объект должен содержать все четыре.
interface Person в одном файле сливаются в один тип.
Первое объявляет name: string и age: number, второе объявляет address: string и email: string. Создайте объект person со всеми четырьмя свойствами (John, 30, "123 Main St", [email protected]) и выведите объект целиком.
Вывод: программа на TypeScript и захваченный вывод
// Первое объявление
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: "[email protected]"
};
console.log(person);
Перегрузки функций и слияние
Можно написать несколько сигнатур функции с одним именем. Они сливаются в одну перегруженную функцию, а все случаи обрабатывает одна реализация.
Пример
У processValue три сигнатуры перегрузки и одна реализация. Перегрузки точно сообщают вызывающему коду, какой тип возвращаемого значения соответствует каждому типу аргумента.
processValue — одна принимает и возвращает string, другая number, третья boolean — а затем одну реализацию, которая принимает string | number | boolean.
Реализация переводит строки в верхний регистр, удваивает числа и инвертирует логические значения. Вызовите её с "hello", 10 и true и выведите каждый результат.
Вывод: программа на TypeScript и захваченный вывод
// Перегрузки функции
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
Слияние пространств имён
Пространства имён с одинаковым именем тоже сливаются. Экспортированные члены одного блока видны в остальных.
Пример
Второй блок Validation может реализовать StringValidator из первого блока, а внешний код обращается ко всему через единое объединённое пространство имён.
namespace Validation сливаются в одно пространство имён.
Первый экспортирует интерфейс StringValidator с методом isValid(s: string): boolean; второй экспортирует интерфейс NumberValidator и класс ZipCodeValidator, который реализует StringValidator, проверяя, что строка состоит ровно из пяти цифр. Создайте ZipCodeValidator через Validation.ZipCodeValidator и выведите результат isValid для "12345", "1234" и "abcde".
Вывод: программа на TypeScript и захваченный вывод
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
Слияние класса и интерфейса
Объявление класса может слиться с интерфейсом с тем же именем. Интерфейс добавляет члены к типу класса, и класс обязан их реализовать.
Слияние класса с интерфейсом
Интерфейс Cart объявляет calculateTotal(), а класс Cart реализует этот метод вместе со своими собственными членами items и addItem.
// 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]) работает по всему объединённому перечислению.
enum Direction сливаются в одно перечисление.
Первое объявляет North и South (автоматическая нумерация 0 и 1); второе объявляет East = 2 и West = 3. Выведите числовое значение всех четырёх членов, а затем выведите Direction[0] и Direction[2], чтобы показать обратный поиск по значению.
Вывод: программа на TypeScript и захваченный вывод
// Первая часть перечисления
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"
Расширение модуля
Существующий модуль или библиотеку можно расширить, объявив дополнительные типы и функции с тем же именем. Это называется расширением модуля (module augmentation).
Пример
Первый блок declare namespace LibraryModule изображает стороннюю библиотеку. Второй блок добавляет новый интерфейс, новое необязательное свойство в User и новую функцию. Так как блоки declare не содержат кода времени выполнения, в примере к globalThis привязывается небольшая заглушка-реализация, чтобы файл можно было запустить.
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 и захваченный вывод
// Исходное определение библиотеки
// Представьте, что оно приходит из сторонней библиотеки
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;
}
// Заглушка времени выполнения для библиотеки (обычно её предоставляет установленный пакет)
(globalThis as any).LibraryModule = {
getUser: (id: number) => ({ id, name: "John", preferences: { theme: "тёмная", notifications: true } }),
getUserPreferences: (userId: number) => ({ theme: "тёмная", notifications: true })
};
// Использование расширенного модуля
const user = LibraryModule.getUser(123);
console.log(user.preferences?.theme);
const prefs = LibraryModule.getUserPreferences(123);
console.log(prefs.notifications);
Лучшие практики
При использовании слияния объявлений стоит помнить о нескольких правилах:
- Порядок важен для перегрузок функций: сигнатура реализации должна быть самой общей.
- Нефункциональные члены должны быть совместимы: если два интерфейса объявляют свойство с одинаковым именем, типы должны быть одинаковыми или совместимыми.
- Более поздние интерфейсы имеют приоритет: если объединяемые интерфейсы конфликтуют, побеждает последнее объявление.
- Приватные и защищённые члены: классы не могут слиться, если у них есть private- или protected-члены с одинаковым именем, но разными типами.
- Экспорт из пространства имён: после слияния снаружи видны только экспортированные объявления.
Вопросы производительности
- Время компиляции: активное использование слияния объявлений может замедлить компиляцию.
- Проверка типов: сложные объединённые типы могут замедлить работу редактора.
- Размер сборки: слияние объявлений никак не влияет на производительность во время выполнения и на размер сборки.
Советы по оптимизации:
- Держите объединяемые интерфейсы сфокусированными и связными.
- Избегайте глубокой вложенности в объединённых типах.
- Для простых комбинаций типов используйте псевдонимы
typeвместо слияния.