Пошук уроків, статей та іншого контенту
Об’єднуємо декларації одного інтерфейсу та розглядаємо практичні сценарії declaration merging.
Declaration merging — це механізм TypeScript, за якого кілька декларацій із тим самим ім’ям об’єднуються в одну підсумкову декларацію.
Найчастіше це використовується з інтерфейсами. Якщо оголосити один інтерфейс кілька разів в одній області видимості, TypeScript об’єднає всі його властивості та методи.
interface User {
id: number;
name: string;
}
interface User {
email: string;
readonly createdAt: Date;
}
function describeUser(user: User): string {
return `${user.name} <${user.email}>`;
}
const user: User = {
id: 1,
name: "Олена",
email: "olena@example.com",
createdAt: new Date(),
};
console.log(describeUser(user));Після об’єднання TypeScript сприймає User так, ніби його оголосили одним блоком:
interface User {
id: number;
name: string;
email: string;
readonly createdAt: Date;
}Тому об’єкт типу User повинен містити всі обов’язкові властивості з усіх декларацій.
Властивості з різних декларацій додаються до спільного інтерфейсу:
interface Product {
id: number;
}
interface Product {
title: string;
}
const product: Product = {
id: 10,
title: "Keyboard",
};Якщо властивість оголошена в кількох деклараціях, її типи повинні збігатися:
interface Config {
port: number;
}
interface Config {
port: number;
}Це коректно, оскільки port має однаковий тип.
Але різні типи спричинять помилку:
interface Config {
port: number;
}
interface Config {
// Помилка: наступні декларації повинні мати однаковий тип port
port: string;
}Так само несумісними будуть різні модифікатори, наприклад readonly і звичайна властивість.
Якщо в одній декларації властивість необов’язкова, а в іншій — обов’язкова, підсумкова властивість буде обов’язковою:
interface Settings {
theme?: "light" | "dark";
}
interface Settings {
theme: "light" | "dark";
}У результаті theme потрібно обов’язково передати під час створення об’єкта Settings.
Методи з однаковим іменем можуть об’єднуватися як перевантаження:
interface Formatter {
format(value: string): string;
}
interface Formatter {
format(value: number): string;
}Тепер Formatter описує метод format, який можна викликати як із рядком, так і з числом:
function useFormatter(formatter: Formatter): void {
console.log(formatter.format("TypeScript"));
console.log(formatter.format(2026));
}Однак об’єкт, який реалізує такий інтерфейс, повинен підтримувати всі оголошені варіанти виклику.
Під час об’єднання декларацій порядок перевантажень має значення: декларації, оголошені пізніше, зазвичай розглядаються раніше під час вибору відповідного сигнатурного варіанта.
Однакове ім’я саме по собі не гарантує об’єднання. Декларації повинні перебувати в сумісній області видимості.
Це найпростіший випадок:
interface ApiResponse {
status: number;
}
interface ApiResponse {
data: unknown;
}Ці декларації об’єднаються.
Якщо файли не містять import або export, їхні декларації можуть належати до глобальної області видимості й об’єднуватися між файлами.
Наприклад, у першому файлі:
interface AppConfig {
apiUrl: string;
}А в другому:
interface AppConfig {
retryCount: number;
}
const config: AppConfig = {
apiUrl: "https://api.example.com",
retryCount: 3,
};У сучасних проєктах більшість файлів є модулями, тому глобального об’єднання краще не створювати без необхідності. Явне розширення глобальних типів виконується за допомогою declare global.
Однакові назви інтерфейсів у двох окремих модулях не об’єднуються:
// user.ts
export interface User {
id: number;
}// account.ts
export interface User {
email: string;
}Це два різні типи User, оскільки кожен із них належить до свого модуля.
Щоб розширити тип із вже наявного модуля, використовують module augmentation — розширення декларації модуля.
Браузерне середовище вже містить глобальний інтерфейс Window. Якщо застосунок додає власну властивість до window, TypeScript не знає про неї автоматично.
Створимо файл типів:
// window-types.ts
export {};
declare global {
interface Window {
appConfig: {
apiUrl: string;
environment: "development" | "production";
};
}
}Після цього в іншому файлі властивість буде типізована:
// app.ts
window.appConfig = {
apiUrl: "https://api.example.com",
environment: "production",
};
console.log(window.appConfig.apiUrl);declare global не створює властивість під час виконання. Він лише повідомляє TypeScript про властивість, яка має існувати в середовищі виконання.
Тому реальне створення window.appConfig все одно має відбутися в коді застосунку або в скрипті конфігурації.
Declaration merging часто використовується бібліотеками та плагінами.
Наприклад, бібліотека може оголосити базовий інтерфейс для конфігурації:
interface PluginOptions {
enabled: boolean;
}Плагін може додати власні налаштування:
interface PluginOptions {
cacheDuration: number;
}Після об’єднання користувач повинен передати обидві властивості:
const options: PluginOptions = {
enabled: true,
cacheDuration: 300,
};Такий підхід дає змогу розширювати типи без редагування початкового коду бібліотеки.
У реальних пакетах для цього зазвичай використовують файли декларацій .d.ts і module augmentation. Важливо, щоб розширення завантажувалося TypeScript-компілятором і посилалося саме на потрібний модуль.
Об’єднання декларацій відбувається лише на рівні типів. Інтерфейси не існують у згенерованому JavaScript.
interface Logger {
log(message: string): void;
}
interface Logger {
level: "info" | "error";
}Ці декларації не створюють об’єкт, клас або метод log. Вони лише описують форму значень, які TypeScript дозволяє використовувати як Logger.
Потрібна реальна реалізація:
interface Logger {
log(message: string): void;
}
interface Logger {
level: "info" | "error";
}
const logger: Logger = {
level: "info",
log(message) {
console.log(`[${this.level}] ${message}`);
},
};
logger.log("Застосунок запущено");Інтерфейси підтримують повторне оголошення, а псевдоніми типів — ні:
interface User {
id: number;
}
interface User {
name: string;
}Це коректне об’єднання.
Для type повторне оголошення з таким самим ім’ям заборонене:
type Product = {
id: number;
};
// Помилка: Duplicate identifier "Product"
type Product = {
title: string;
};Якщо потрібно комбінувати типи, для type використовують перетин:
type Identifiable = {
id: number;
};
type Named = {
name: string;
};
type User = Identifiable & Named;Вибір між повторним оголошенням інтерфейсу та перетином типів залежить від завдання:
declaration merging зручний для розширення публічного контракту або типів бібліотеки;
перетин типів зручний, коли потрібно явно скласти новий тип із кількох незалежних частин.
Однакові імена в різних модулях не означають однаковий тип. Для розширення імпортованого типу потрібен module augmentation.
Одна й та сама властивість у деклараціях повинна мати сумісний тип:
interface Options {
timeout: number;
}
interface Options {
// Помилка
timeout: string;
}Якщо додати нову обов’язкову властивість до інтерфейсу, усі наявні об’єкти цього типу перестануть відповідати йому:
interface User {
id: number;
}
interface User {
email: string;
}
// Тепер email є обов’язковою властивістю
const user: User = {
id: 1,
};Для поступового розширення контракту іноді доречніше використовувати необов’язкову властивість:
interface User {
email?: string;
}Declaration merging не додає властивості до JavaScript-об’єктів і не створює методи. Воно змінює лише перевірку типів під час компіляції.
У модульному файлі глобальний інтерфейс потрібно розширювати всередині declare global:
export {};
declare global {
interface Window {
featureEnabled: boolean;
}
}Без цього декларація може залишитися локальною для модуля або не мати очікуваного ефекту.
Declaration merging об’єднує кілька декларацій інтерфейсу з однаковим ім’ям.
Властивості з усіх декларацій потрапляють до підсумкового інтерфейсу.
Однакові властивості повинні мати сумісні типи.
Методи з однаковим іменем можуть утворювати перевантаження.
Інтерфейси з однаковим ім’ям у різних модулях автоматично не об’єднуються.
Для розширення глобальних або бібліотечних типів використовують declare global чи module augmentation.
Declaration merging працює лише на рівні типів і не змінює JavaScript під час виконання.
Псевдоніми типів type не підтримують повторне оголошення; для них використовують перетини типів.