Skip to main content
Практика завдань

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.

typescript
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. Але після оголошення його не можна відкрити знову.

typescript
type Status = "active" | "inactive"; // union type ID = string | number; // union примітивів type Tagged<T> = T & { _tag: string }; // intersection

interface Status = "active" | "inactive" є синтаксичною помилкою. Interface не є union type.

Що відбувається всередині компілятора

  1. Parsing: І interface Person {}, і type Person = {} будують AST-вузли. На цьому етапі вони схожі.
  2. Binding: Символи interface отримують прапорець mergeable. Кілька оголошень з однаковою назвою об'єднують свої члени. Дублікат type alias дає помилку компіляції тут же.
  3. Type checking: interface Employee extends Person створює відношення підтипу. type Employee = Person & { position: string } обчислює явне intersection. Обидва дають однаковий набір членів для об'єктів.
  4. Emission: Жоден не генерує runtime-коду. Обидва повністю зникають зі скомпільованого JavaScript.

Ключові відмінності

interfacetype
Форма об'єкта
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 руйнує це припущення:

typescript
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 без ручного приведення типів.

typescript
// 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 може це виразити.

typescript
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 не може представити.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?
Практика завдань