type vs interface в TypeScript
type vs interface в TypeScript - два способи описати форму даних. Головна різниця: interface підтримує злиття декларацій (declaration merging) між файлами, type цього не вміє.
На співбесідах це питання чекає конкретної відповіді. "Обидва описують об'єкти" недостатньо для middle або senior позиції.
Теорія
Навіщо TypeScript має обидва
У JavaScript не було системи типів. Коли TypeScript її додав, потрібно було підтримати два стилі одночасно: об'єктно-орієнтований код з extends та implements, і функціональний код з примітивами, union-ами і tuple-ами. Один конструкт не міг охопити обидва. Тому interface відповідає за ООП, type відповідає за все інше.
Як працює interface
interface оголошує форму об'єкта і отримує від компілятора прапорець "mergeable". Напиши однакову назву interface у двох різних файлах, і TypeScript автоматично об'єднає їх члени. Це і є declaration merging.
interface User { name: string; }
interface User { age: number; }
// TypeScript читає це як: { name: string; age: number; }Саме так влаштований @types/express. Щоб додати userId до кожного об'єкта Request, ти переоголошуєш interface у своєму .d.ts файлі, і він зливається з визначенням пакету автоматично.
Як працює type alias
type alias дає ім'я виразу типу: формі об'єкта, примітиву, union, intersection, tuple або conditional type. Але після оголошення його не можна відкрити знову.
type Status = "active" | "inactive"; // union
type ID = string | number; // union примітивів
type Tagged<T> = T & { _tag: string }; // intersectioninterface Status = "active" | "inactive" є синтаксичною помилкою. Interface не є union type.
Що відбувається всередині компілятора
- Parsing: І
interface Person {}, іtype Person = {}будують AST-вузли. На цьому етапі вони схожі. - Binding: Символи interface отримують прапорець mergeable. Кілька оголошень з однаковою назвою об'єднують свої члени. Дублікат type alias дає помилку компіляції тут же.
- Type checking:
interface Employee extends Personстворює відношення підтипу.type Employee = Person & { position: string }обчислює явне intersection. Обидва дають однаковий набір членів для об'єктів. - Emission: Жоден не генерує runtime-коду. Обидва повністю зникають зі скомпільованого JavaScript.
Ключові відмінності
interface | type | |
|---|---|---|
| Форма об'єкта | ✅ | ✅ |
| Union / примітив | ❌ | ✅ |
| Conditional type | ❌ | ✅ |
| Declaration merging | ✅ | ❌ |
| Розширення | extends | & |
implements у класі | ✅ | ✅ |
interface розширюється через extends, як ієрархія класів. type перетинається через &. Оголосити той самий type alias двічі означає миттєву помилку компіляції. Тільки type може бути псевдонімом примітива, union, tuple або conditional type.
Де кожен живе в реальних проєктах
Бібліотеки відкривають interface. Додатки використовують type alias для більшості внутрішньої логіки.
React і Chakra UI оголошують interface Props у файлах компонентів, щоб споживачі могли розширювати їх через declaration merging. Всередині React Hook Form, UseFormReturn<T> є type alias з умовною логікою: T extends object ? { fields: T; reset: () => void } : never. Interface не може виразити таку форму.
Я бачив кодові бази, де скрізь використовують тільки type. Це працює, але втрачаєш можливість розширювати сторонні визначення без обгорток.
Два поширені misconceptions
Перший: оскільки обидва описують форми об'єктів, вони взаємозамінні. Declaration merging руйнує це припущення:
type ID = { id: number };
type ID = { id: string }; // Error: Duplicate identifier 'ID'
interface ID { id: number; }
interface ID { id: string; } // Працює: TypeScript зливає до { id: number | string }Другий: є різниця у runtime-продуктивності. Її немає. Обидва повністю стираються до JavaScript. Вибір між type і interface ніяк не впливає на розмір бандлу або швидкість виконання.
Що вивчати далі
Declaration merging веде до module augmentation, саме так додають кастомні властивості до Express.Request у реальних проєктах. Utility types на кшталт Partial<T> і Record<K, V> побудовані на type alias, тому розуміння type спрощує вивчення utility types. Generics у TypeScript стають зрозумілішими, коли знаєш навіщо взагалі існують type alias.
Приклади
Розширення Express Request через declaration merging
Найпоширеніший реальний кейс для declaration merging. Потрібно, щоб req.userId був доступний у кожному route handler без ручного приведення типів.
// types/express.d.ts
declare namespace Express {
interface Request {
userId?: string; // зливається з Express.Request глобально
}
}
// routes/profile.ts
const getProfile = (req: Express.Request, res: Express.Response): void => {
console.log(req.userId); // TypeScript знає про цю властивість
};Властивість доступна скрізь, бо interface злився. Type alias не може цього зробити. Без merging довелося б створити окремий AuthRequest і приводити тип у кожному handler вручну.
Conditional return type через type alias
Функція, яка повертає різні форми залежно від generic-параметра. Тільки type може це виразити.
interface FormField {
name: string;
value: string | number;
}
// Conditional type - interface не може це представити
type UseFormReturn<T> = T extends object
? { fields: T; reset: () => void }
: never;
type LoginForm = { email: string; password: string };
const form = {} as UseFormReturn<LoginForm>;
// form.fields.email - TypeScript виводить точну форму
// UseFormReturn<string> зводиться до never - помилка на етапі компіляціїinterface FormField правильний тут, бо інші пакети можуть його розширити. type UseFormReturn правильний, бо використовує conditional вираз, який interface не може представити.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.