Тип vs інтерфейс: Що використовувати у 2023 році?

Тип vs інтерфейс: Що використовувати у 2023 році?
Переклад 6 хв. читання
25 серпня 2023

Коротке пояснення

За замовчуванням слід використовувати типи, доки вам не знадобиться специфічна особливість інтерфейсів, наприклад, extends.

  • Інтерфейси не можуть виражати об'єднання, маповані або умовні типи. Псевдоніми типів можуть виражати будь-який тип.
  • Інтерфейси можуть використовувати розширення, типи - ні.
  • Коли ви працюєте з об'єктами, які успадковують один від одного, використовуйте інтерфейси. extends робить перевірку типу TypeScript трохи швидшою, ніж використання &.
  • Інтерфейси з однаковими іменами в одній області видимості об'єднують свої оголошення, що призводить до неочікуваних помилок.
  • Псевдоніми типів мають неявну індексну сигнатуру Record, яка іноді зустрічається.

Повне пояснення

TypeScript пропонує першокласний примітив для визначення об'єктів, які розширюються з інших об'єктів - інтерфейс.

Інтерфейси були присутні з найпершої версії TypeScript. Вони базуються на об'єктно-орієнтованому програмуванні та дозволяють використовувати успадкування для створення типів:

interface WithId {
  id: string;
}
 
interface User extends WithId {
  name: string;
}
 
const user: User = {
  id: "123",
  name: "Karl",
  wrongProperty: 123,
}
Тип { id: string; name: string; wrongProperty: number; } не може бути присвоєний типу User. Літерал об'єкта може вказувати лише відомі властивості, а wrongProperty не існує в типі User.

Однак у них є вбудована альтернатива - псевдоніми типів, що оголошуються за допомогою ключового слова type. Ключове слово type можна використовувати для представлення будь-якого типу в TypeScript, а не лише об'єктних типів.

Скажімо, ми хочемо представити тип, який є рядком або числом. Ми не можемо зробити це за допомогою інтерфейсу, але можемо за допомогою типу:

type StringOrNumber = string | number;
 
const func = (arg: StringOrNumber) => {};
 
func("hello");
func(123);
 
func(true);
Argument of type 'boolean' is not assignable to parameter of type 'StringOrNumber'.

Але, звичайно, псевдоніми типів також можна використовувати для опису об'єктів. Серед користувачів TypeScript це викликає багато суперечок. Чи слід використовувати інтерфейс або псевдонім типу, коли ви оголошуєте тип об'єкта?

Використання інтерфейсів для наслідування

Якщо ви працюєте з об'єктами, які наслідують один одного, використовуйте інтерфейси. Наш приклад вище, з використанням WithId, можна виразити за допомогою псевдонімів типів, використовуючи тип intersection.

type WithId = {
  id: string;
};
 
type User = WithId & {
  name: string;
};
 
const user: User = {
  id: "123",
  name: "Karl",
  wrongProperty: 123,
}
Тип { id: string; name: string; wrongProperty: number; } не може бути присвоєний типу User. Об'єктний літерал може вказувати лише відомі властивості, а wrongProperty не існує в типі User.

Це цілком нормальний код, але він дещо менш оптимальний. Причина полягає у швидкості, з якою TypeScript може перевіряти ваші типи.

Коли ви створюєте інтерфейс з використовуючи extends, TypeScript може кешувати цей інтерфейс за його назвою у внутрішньому реєстрі. Це означає, що майбутні перевірки можуть бути виконані швидше. З типом перетину, що використовує &, він не може кешувати його за назвою - йому доводиться обчислювати його майже кожного разу.

Це невелика оптимізація, але якщо інтерфейс використовується багато разів, це має значення. Ось чому вікі про продуктивність TypeScript рекомендує використовувати інтерфейси для успадкування об'єктів - і я також рекомендую.

Однак, я все одно не рекомендую використовувати інтерфейси за замовчуванням. Чому?

Інтерфейси можуть об'єднувати декларації

Інтерфейси мають ще одну особливість, яка, якщо ви до неї не підготовлені, може здатися дуже дивною.

Коли два інтерфейси з однаковими іменами оголошуються в одній області видимості, вони об'єднують свої оголошення.

interface User {
  name: string;
}
 
interface User {
  id: string;
}
 
const user: User = {
  id: "123",
};
Помилка: Property 'name is missing in type { id: string; } but required in type User.

Якби ви спробували зробити це з типами, це б не спрацювало:

type User = {
  name: string;
};
 
type User = {
  id: string;
};
Помилка: Duplicate identifier 'User'.

Це передбачувана поведінка і необхідний елемент мови. Вона використовується для моделювання бібліотек JavaScript, які модифікують глобальні об'єкти, наприклад, додають методи до прототипів рядків.

Але якщо ви не готові до цього, це може призвести до дійсно заплутаних помилок.

Якщо ви хочете уникнути цього, я рекомендую вам додати ESLint до вашого проєкту та увімкнути правило no-redeclare.

Сигнатури індексів у типах та інтерфейсах Ще одна відмінність між інтерфейсами та типами є малопомітною.

Псевдоніми типів мають неявну індексну сигнатуру, а інтерфейси - ні. Це означає, що їх можна присвоювати типам, які мають індексну сигнатуру, але не інтерфейсам. Це може призвести до помилок типу:

Помилка: Index signature for type 'string' is missing in type 'x'.
interface KnownAttributes {
  x: number;
  y: number;
}
 
const knownAttributes: KnownAttributes = {
  x: 1,
  y: 2,
};
 
type RecordType = Record<string, number>;
 
const oi: RecordType = knownAttributes;
Помилка: Type 'KnownAttributes' is not assignable to type 'RecordType'. Index signature for type 'string' is missing in type 'KnownAttributes'.

Причиною цієї помилки є те, що інтерфейс згодом може бути розширено. До нього може бути додана властивість, яка не збігається з ключем рядка або значенням числа.

Ви можете виправити це, додавши до інтерфейсу явну сигнатуру індексу:

interface KnownAttributes {
  x: number;
  y: number;
  [index: string]: unknown; // new!
}

Або просто змінити його на тип use:

type KnownAttributes = {
  x: number;
  y: number;
};
 
const knownAttributes: KnownAttributes = {
  x: 1,
  y: 2,
};
 
type RecordType = Record<string, number>;
 
const oi: RecordType = knownAttributes;

Хіба це не дивно?

За замовчуванням вибирайте тип, а не інтерфейс

У документації до TypeScript є чудовий посібник з цього питання. Вони описують кожну функцію (хоча і не неявний підпис індексу), але приходять до іншого висновку, аніж я.

Вони рекомендують вибирати на основі особистих уподобань, з чим я згоден. Різниця між типом та інтерфейсом досить мала, щоб ви могли використовувати будь-який з них без особливих проблем.

Але команда TS рекомендує вам за замовчуванням використовувати інтерфейс, а тип використовувати лише тоді, коли це необхідно.

Я б хотів рекомендувати протилежне. Можливості об'єднання оголошень та неявних сигнатур індексів є достатньо дивними, щоб відлякати вас від використання інтерфейсів за замовчуванням.

Інтерфейси все ще залишаються моєю рекомендацією для успадкування об'єктів, але я б рекомендував вам використовувати тип за замовчуванням. Це трохи гнучкіше і трохи менш несподівано.

Джерело: Type vs Interface: Which Should You Use In 2023?
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід