Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Відмінності між type та interface в TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`type` vs `interface`** у TypeScript: `interface` підтримує злиття декларацій (declaration merging) і наслідування через `extends`; `type` підтримує union-типи, примітиви і трансформації типів. ```typescript interface User { name: string } interface User { age: number } // об'єднується автоматично type Status = "active" | "inactive"; // тільки type може це type ID = string | number; // псевдонім примітиву ``` **Головне:** `interface` - для контрактів об'єктів, які планується розширювати; `type` - для union-типів, примітивів і всього, що не є простою формою об'єкта.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`type` та `interface`** у TypeScript обидва описують форму об'єктів, але поводяться по-різному: `interface` автоматично об'єднує повторні декларації, а `type` створює фіксований псевдонім, що підтримує union-типи, примітиви і mapped types. ## Теорія ### TL;DR - `interface` = Google Doc (можна дописати будь-коли); `type` = PDF (зафіксований після створення) - Головна різниця: `interface` автоматично об'єднує повторні декларації, `type` дає помилку при повторному оголошенні - Тільки `type` підтримує union-типи (`A | B`), примітиви, кортежі та mapped types - Потрібні контракти об'єктів, наслідування або аугментація бібліотек? `interface`. Union, discriminated union або трансформації типів? `type` - Обидва повністю видаляються під час компіляції. В рантаймі їх немає ### Швидкий приклад ```typescript // interface: повторні декларації об'єднуються interface User { name: string; } interface User { age: number; // об'єднується з декларацією вище } // Результат: User = { name: string; age: number } // type: повторна декларація - помилка type Product = { id: number }; // type Product = { name: string }; // ПОМИЛКА: Duplicate identifier 'Product' // Тільки type підтримує union-типи type Status = "active" | "inactive" | "pending"; type Result = User | Product; ``` Оголоси `User` двічі через `interface` - TypeScript об'єднає їх в один тип. Зроби те саме з `type` - компілятор відмовить. ### Головна різниця `interface` призначений для опису форми об'єктів. Він підтримує злиття декларацій (declaration merging), коли дві декларації з однаковою назвою об'єднуються в одну, і наслідування через `extends`. `type` - загальний псевдонім для будь-якої структури: примітиви, union-типи, перетини (intersection), кортежі. Але його не можна повторно оголосити чи злити. Після `type X = ...` це визначення фінальне. ### Коли що використовувати - **`interface`:** контракти об'єктів для класів (`implements`), форми API-відповідей, React props, які інші компоненти можуть розширювати, аугментація бібліотек - **`type`:** union-типи (`"success" | "error"`), перетини (`A & B`), псевдоніми примітивів (`type ID = string | number`), discriminated union для стейт-машин і reducers, mapped types, conditional types Багато команд за замовчуванням беруть `interface` для об'єктів і `type` для всього іншого. Інші використовують `type` скрізь. Обидва підходи працюють. Найбільший головний біль, який я бачив: хтось публікує бібліотеку з `type` для публічних контрактів, які споживачі мали б розширювати. Такий вибір відгукується в кожному downstream-проекті. ### Таблиця порівняння | Можливість | `interface` | `type` | |---|---|---| | Форма об'єкта | Так | Так | | Union-типи | Ні | Так | | Перетин | Через `extends` | Через `&` | | Злиття декларацій | Так | Ні | | Примітиви / кортежі | Ні | Так | | Сигнатури функцій | Працює, але не прийнято | Так | | Mapped types | Ні | Так | | Conditional types | Ні | Так | | `implements` у класах | Так (стандартно) | Так (працює) | | **Найкраще для** | Контракти об'єктів, класи, аугментація бібліотек | Union-типи, примітиви, трансформації типів | ### Як компілятор обробляє це TypeScript трактує декларації `interface` як відкриті контейнери. Якщо компілятор бачить дві декларації з однаковою назвою, він об'єднує їх властивості в один тип. Псевдоніми `type` розгортаються як пряма підстановка: компілятор замінює кожне посилання на псевдонім його визначенням. Жоден з них не виживає після компіляції. Обидва повністю видаляються, коли TypeScript транспілює код у JavaScript. ### Типові помилки **Спроба створити union через `interface`** ```typescript // НЕПРАВИЛЬНО - синтаксична помилка // interface Result = User | Product; // ПРАВИЛЬНО type Result = User | Product; ``` `interface` не може виразити "або A, або B". Це концепція `type`. --- **Очікування злиття від `type`** ```typescript // НЕПРАВИЛЬНО type Config = { apiUrl: string }; type Config = { timeout: number }; // ПОМИЛКА: Duplicate identifier // ПРАВИЛЬНО - interface, якщо потрібне злиття interface Config { apiUrl: string; } interface Config { timeout: number; } // об'єднується автоматично const config: Config = { apiUrl: "...", timeout: 5000 }; // працює ``` --- **Опціональні властивості замість discriminated union** ```typescript // НЕПРАВИЛЬНО - дозволяє неможливі комбінації стану interface State { status: "idle" | "loading" | "success" | "error"; data?: User; error?: string; // ніщо не забороняє мати і data, і error одночасно } // ПРАВИЛЬНО - discriminated union робить невалідні стани неможливими type State = | { status: "idle" } | { status: "loading" } | { status: "success"; data: User } | { status: "error"; error: string }; ``` Цей патерн вимагає `type`. Аналога через `interface` немає. --- **`&` замість `extends` для наслідування об'єктів** ```typescript // Працює, але приховує ієрархію type AdvancedConfig = BaseConfig & { ssl: boolean }; // Зрозуміліше, якщо ієрархія має значення interface AdvancedConfig extends BaseConfig { ssl: boolean; } ``` Тут є реальна різниця в поведінці. `extends` одразу видає помилку при конфлікті властивостей. З `&` конфліктний тип стає `never` - тихий глухий кут без помилки, який проявиться тільки коли спробуєш використати цю властивість. --- **`interface` для сигнатур функцій** ```typescript // Працює, але виглядає незвично interface Callback { (error: Error | null, data: string): void; } // Чистіше і поширеніше type Callback = (error: Error | null, data: string) => void; ``` ### Реальне застосування - **React:** props через `interface` розширюють `React.PropsWithChildren`; стан компонента використовує discriminated `type` union - **Redux:** типи екшенів - discriminated `type` union для type-safe reducers; форма стору - `interface` - **Express:** middleware додає властивості до `Request` через злиття декларацій `interface`; обробники маршрутів використовують `type` для union-відповідей - **NestJS:** DTO через `interface` для наслідування; API-відповіді через `type` для discriminated union - **Автори бібліотек:** всі публічні контракти як `interface`, щоб споживачі могли їх аугментувати без зміни вихідного коду ### Питання для поглиблення **Q:** Чи можна використовувати `type` в `implements` у класі? **A:** Так, структурно це працює. Але `interface` - стандартний вибір, бо сигналізує про намір і підтримує злиття декларацій. Використання `type` з `implements` не помилка, просто не прийнято. **Q:** Що відбувається, коли при `extends` є конфлікт властивостей? **A:** TypeScript одразу видає помилку в місці `extends`. З `&` в `type` конфліктний тип стає `never` замість помилки. Різниця важлива: `never` - тихий глухий кут, `extends` - чітка помилка прямо в місці конфлікту. **Q:** Як злиття декларацій (declaration merging) працює з generics? **A:** Параметри generics мають збігатися точно між злитими деклараціями. Два `interface Box<T>` зливаються нормально. Але `interface Box<T, U>` і `interface Box<T>` - це різні інтерфейси, вони не зіллються. **Q:** Чому Express, NestJS і Fastify використовують `interface` для публічних типів? **A:** Бо споживачі мають їх розширювати. Express middleware додає властивості до `Request`, перевизначаючи `interface` в локальному коді. Якби Express використав `type Request = { ... }`, така аугментація не компілювалась би. **Q:** (Senior) Як спроектувати публічну систему типів, щоб споживачі могли розширювати їх без зміни твого коду? **A:** Експортуй всі публічні контракти як `interface`, а не `type`. Це дає споживачам змогу використовувати злиття декларацій у своїх файлах - вони додають властивості, перевизначаючи твій інтерфейс. З `type` вони обмежені тим, що ти їм дав. Саме так працює аугментація `Request` в Express. ## Приклади ### Розширення об'єкта через `interface` ```typescript interface BaseProps { className?: string; children: React.ReactNode; } interface ButtonProps extends BaseProps { onClick: () => void; variant: "primary" | "secondary"; } const Button: React.FC<ButtonProps> = ({ onClick, variant, children, className, }) => ( <button onClick={onClick} className={`btn btn-${variant} ${className}`}> {children} </button> ); ``` `ButtonProps` автоматично отримує `className` і `children` з `BaseProps`. Ключове слово `extends` одразу показує ієрархію і відловлює конфлікти під час компіляції. ### Discriminated union через `type` (стейт-машина API) ```typescript type ApiResponse = | { status: "success"; data: User[] } | { status: "error"; error: string } | { status: "loading" }; function handleResponse(response: ApiResponse) { switch (response.status) { case "success": console.log(response.data); // TypeScript знає, що data існує тут break; case "error": console.error(response.error); // TypeScript знає, що error існує тут break; case "loading": console.log("Чекаємо..."); break; } } ``` TypeScript використовує поле `status` для звуження типу в кожній гілці. Додай новий статус і забудь його обробити - TypeScript помітить пропуск. ### Аугментація бібліотеки через злиття декларацій ```typescript declare global { namespace Express { interface Request { user?: { id: string; role: "admin" | "viewer"; }; } } } app.get("/profile", (req, res) => { if (req.user?.role === "admin") { // TypeScript знає, що user.role має рівно два можливих значення } }); ``` Це працює тому, що Express експортує `Request` як `interface`. Блок `declare global` зливає наші доповнення з існуючим визначенням. З `type` цей код не компілювався б.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.