Обидва способи складають тип з кількох частин:
interface HasId { id: number }
interface HasTimestamps { createdAt: string; updatedAt: string }
// успадкування інтерфейсів
interface Post extends HasId, HasTimestamps {
title: string;
}
// перетин типів
type Comment = HasId & HasTimestamps & { body: string };
Для простих випадків результат однаковий. Різниця - у конфліктах і в тому, як компілятор з ними працює.
1. Конфлікт властивостей.
interface A { status: string }
interface B extends A { status: number } // помилка одразу: number не сумісний з string
type C = { status: string } & { status: number }; // помилки немає...
// ...але C['status'] - це never: жодне значення не підійде, і об'єкт типу C не створити
extends повідомляє про несумісність в оголошенні. Перетин мовчки дає never, і помилка з'являється далеко - там, де намагаються створити об'єкт.
2. Продуктивність перевірки типів. Інтерфейси кешуються як іменовані типи; компілятор порівнює їх за назвою. Великі перетини перераховуються щоразу. Документація TypeScript з продуктивності радить у великих кодових базах складати об'єктні типи через interface ... extends, а не через довгі ланцюжки &.
3. Що можна скласти. extends в інтерфейсі - лише з об'єктними типами (й класами). Перетин працює з будь-якими типами, включно з об'єднаннями й узагальненнями:
type WithMeta<T> = T & { meta: { requestId: string } };
type Branded = string & { readonly __brand: 'UserId' };
Перетин з об'єднаннями розподіляється: (A | B) & C - це (A & C) | (B & C).
Перетин примітивів - string & number - дає never: значення, що водночас рядок і число, не існує.
Практичні правила:
- об'єктні сутності й їх розширення -
interface ... extends: краща діагностика й швидкість; - узагальнені «домішки» (
T & { ... }), брендовані типи, композиція типів з утиліт - перетин; - якщо перетин дає несподіваний
never, - майже завжди конфлікт однойменних властивостей.