Система типів TypeScript фундаментально спирається на структурну типізацію (Structural Typing / Duck Typing): сумісність типів визначається виключно їхньою формою (набором властивостей і методів), а не явним номінальним оголошенням (як у Java або C#). Якщо два різні типи мають однакову структуру полів, компілятор вважає їх взаємозамінними.
Для опису форм структурних даних у TypeScript існують два основні інструменти:
interface): відкриті контракти для опису об'єктів і класів. Підтримують успадкування через extends та механізм автоматичного злиття декларацій (Declaration Merging).type): гнучкі синтаксичні псевдоніми для будь-яких сутностей системи типів (об'єктів, примітивів, об'єднань union, перетинів intersection, кортежів tuples та функціональних сигнатур).У сучасній розробці вибір між interface та type диктується архітектурним контекстом: інтерфейси ідеальні для розширюваних публічних контрактів бібліотек та ООП-ієрархій, а псевдоніми типів незамінні при створенні складних композиційних та обчислюваних типів.
Різниця між interface та type зводиться до відкритості (відкриті для злиття) проти фіксації (незмінне визначення):
+-------------------------------------------------------------------------+
| INTERFACE vs TYPE: МЕНТАЛЬНА МОДЕЛЬ |
+-------------------------------------------------------------------------+
| INTERFACE (Відкритий контракт): |
| interface User { id: string } |
| interface User { name: string } ---> Результат: { id: string; name: string }|
| (Злиття декларацій Declaration Merging працює автоматично) |
+-------------------------------------------------------------------------+
| TYPE ALIAS (Замкнутий псевдонім): |
| type User = { id: string } |
| type User = { name: string } ---> ПОМИЛКА TS2300: Duplicate identifier|
| (Повторне оголошення суворо заборонено) |
+-------------------------------------------------------------------------+
| ЗДІБНОСТІ ДО КОМПОЗИЦІЇ: |
| type Role = 'admin' | 'user' ---> Інтерфейс НЕ вміє робити union! |
| type Pair = [number, string] ---> Інтерфейс НЕ вміє робити кортеж! |
+-------------------------------------------------------------------------+&)
Компілятор зіставляє список необхідних властивостей цільового типу з фактичним значенням.
Для кожної властивості рекурсивно перевіряється сумісність типів її значень.
Для прямих об'єктних літералів компілятор блокує невідомі поля, запобігаючи друкарським помилкам.
Результат перевірки зв'язку типів зберігається у внутрішньому кеші компілятора для прискорення збірки.
Що станеться, якщо в різних файлах проєкту оголошено два інтерфейси з однаковим ім'ям: `interface Window { analytics: Tracker }`?
process.env у Node.js або глобального об'єкта window через злиття інтерфейсів.req.user до базового інтерфейсу запиту.IUserRepository) для реалізації патерну інверсії залежностей (Dependency Inversion).type TenantEntity = BaseEntity & Auditable & TenantBound).extends vs &#Інтерфейси розширюються за допомогою ключового слова extends, а типи об'єднуються через оператор перетину &:
// 1. Інтерфейси: успадкування через extends
interface Identified {
id: string;
}
interface User extends Identified {
email: string;
role: 'admin' | 'user';
}
// 2. Type Aliases: композиція через перетин (&)
type Timestamped = {
createdAt: Date;
updatedAt: Date;
};
type FullUser = User & Timestamped;
const activeUser: FullUser = {
id: 'usr_9482',
email: '[email protected]',
role: 'admin',
createdAt: new Date(),
updatedAt: new Date(),
};Інтерфейс суворо перевіряє сумісність властивостей, що перевизначаються. У разі несумісності компілятор одразу видає помилку. У випадку type & компілятор створює тип never для конфліктуючої властивості:
interface BaseNode {
id: string;
}
// ПОМИЛКА: interface явно забороняє несумісне перевизначення
// interface NumberNode extends BaseNode {
// id: number; // TS2430: Interface 'NumberNode' incorrectly extends interface 'BaseNode'.
// }
// У Type Alias перетин несумісних примітивів створює 'never':
type Alpha = { id: string };
type Beta = { id: number };
type Combined = Alpha & Beta;
// Поле id має тип: string & number === never!
// const bad: Combined = { id: 123 }; // ПОМИЛКА: Type 'number' is not assignable to type 'never'.type, але не вміє interface#Псевдоніми типів можуть представляти примітиви, кортежі, об'єднання та обчислювані властивості (Mapped Types):
// Об'єднання (Unions)
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
// Кортежі (Tuples)
type Coordinate = [latitude: number, longitude: number];
// Примітивні псевдоніми
type UUID = string;
// Вилучення типів властивостей (Indexed Access)
type UserRole = User['role']; // 'admin' | 'user'
// Відображені типи (Mapped Types) - всередині інтерфейсу заборонені!
type ReadonlyRecord<T> = {
readonly [K in keyof T]: T[K];
};Чому вираз `const user: User = { id: '1', email: '[email protected]', extra: true }` викликає помилку, але при передачі через проміжну змінну `const temp = { id: '1', email: '[email protected]', extra: true }; const user: User = temp;` компілятор мовчить?
Якщо дві різні команди оголошують інтерфейс UserConfig у глобальній області видимості, TypeScript злиє їхні поля воєдино. Це може маскувати приховані баги та колізії.
Рішення: Використовуйте type для внутрішніх структур або упаковуйте інтерфейси в іменовані ES-модулі з export.
При сліпому злитті типів через & легко створити властивості типу never:
type ResA = { status: 200; data: string };
type ResB = { status: 404; error: string };
type Res = ResA & ResB; // Поле status стало (200 & 404) === never!Для взаємовиключних станів завжди використовуйте об'єднання (|), а не перетин (&).