Якщо ви працюєте з TypeScript, то, ймовірно, вже стикалися з питанням: що краще використовувати для оголошення типів — type
чи interface
? Сьогодні ми в цьому розберомось
Тут тільки type
По-перше, варто зазначити, що питання вибору між type і interface не актуальне, коли мова йде про типи, де використання type
є нашою єдиною опцією.
Types
Ключове слово type
використовується для створення псевдонімів типів (Type aliases). Це означає, що ми можемо створити псевдоніми для будь-якого типу, включаючи:
Літеральні типи:
type LuckyNumber = 13;
type True = true;
type HeWhoMustNotBeNamed = 'Voldemort';
Примітивні типи:
type Name = string;
type Choice = boolean;
Кортежі (Tuples)
type MagicalCreatureSighting = [creatureName: string, location: string, isDangerous: boolean, numberSeen: number];
Об’єднання типів (Union types):
type PowerfulWizard = "Albus Dumbledore" | HeWhoMustNotBeNamed;
Mapped Types:
type MyPartial<T> = { [P in keyof T]?: T[P]; };
Функції:
type CastSpell = (spell: "Expelliarmus" | "Lumos" | "Alohomora", target: string) => void;
Обʼєкти
type Wizard = {
name: string;
house: 'Gryffindor' | 'Hufflepuff' | 'Ravenclaw' | 'Slytherin';
age: number;
knownSpells: string[];
pet?: 'Owl' | 'Cat' | 'Toad';
}
Тобто за допомогою type ми можемо оголосити будь який типи - на те вони і псевдоніми для типів.
Interfaces
Натомість, Інтерфейси (interfaces) у TypeScript призначені виключно для визначення структури об’єктів і функцій.
Тому питання вибору між type
і interface
виникає лише тоді, коли ми працюємо з об’єктами або функціями. В усіх інших випадках, коли мова йде про інші типи, наша єдина опція — це type
.
Функції
Що ж стосується функцій і об’єктів? У випадку з функціями питання вибору, як правило, не виникає, оскільки більшість розробників надають перевагу використанню псевдонімів типів (type aliases) через їх більш явний і зрозумілий синтаксис.
Для порівняння:
type CastSpell = (spell: "Expelliarmus" | "Lumos" | "Alohomora", target: string) => void;
interface CastSpell {
(spell: "Expelliarmus" | "Lumos" | "Alohomora", target: string): void;
}
Як бачите, синтаксис type
для функцій виглядає простіше і зрозуміліше.
Об’єкти
А ось для обʼєктів думки можуть різнитися і крім синтаксису є і більш вагомі відмінності, але почнемо ми саме з нього
Синтаксис
- Для
type
- після назви ми ставимо знак рівності, дляinterface
- не ставимо
type Animal = {
name: string
}
interface Animal {
name: string
}
- Для розширення
type
ми вокристовуємо знак амперсанду (в Typescript він назваєтьсяintersection
) - іншими словами, створюємо перетин типів (intersection type). А для розширенняinterface
ми використовуємо звичне для ООП ключове словоextends
type Goat = Animal & {
isSmart: true
}
interface Sheep extends Animal {
isSmart: false
isCute: true
}
Швидкість
Розібравшись синтаксисі, давайте подивимось на більш вагомі відмінності, і почнемо з тих, про які можно прямо прочитати в документації Typescript > Handbook > Everyday Types
Почнемо з швидкості:
Using interfaces with extends can often be more performant for the compiler than type aliases with intersections
Використання інтерфейсів з extends часто може бути продуктивнішим для компілятора, ніж type aliases із перетинами (intersections).
І в посиланні на те як оптимізувати швидкість в Typescript - першою порадою ми бачимо використовувати interface extends замість type intersections.
То ж, що значить на практиці ця різниця в швидкості і коли вона може бути відчутна?
Поперше, треба розуміти, що це не той performance, який впливає на клієнтський досвід користування вашим продуктом - додатком, вебсайтом, тощо.
Це швидкість того, як компілюється ваш TS код. Тож негативний вплив на performance в данному випадку - це збільшення часу, за який буде проходити type check і будуть створюватися артефакти збірки (файли .js і .d.ts ).
Це performance, який може впливати на ваш досвід яки розробника (developer experience/DX) .І найбільшу різницю можна буде відчути наприклад тоді, коли у вас великий проект, який треба регулярно збирати (наприклад, як частину CI процессу), що в свою чергу потребує компіляції всього TS коду.
Наприклад в цьому репозиторії https://github.com/edishu/intersect порівнюється час на компіляцію 10 тисяч інтерфейсів, які рекурсивно використовують extends і 10 тисяч type intersection - як результат варіант з інтерфейсами займає 1 min 26 sec 629 ms, варіант з intersections - 2 min 33 sec 117 ms.
Declaration merging
Наступна цікава відмінність між types
і interfaces
- це те що назвається Declaration merging - який властивий тільки інтерфейсам і в базовому прикладі працює так - якщо ви оголосите інтерфейси з одним імʼям декілька разів, TypeScript автоматично об’єднає їх в один інтерфейс.
interface Animal {
species: string;
sound: string;
}
interface Animal {
favoriteFood: string;
canFly: boolean;
}
const cat: Animal = {
species: 'Cat',
sound: 'Meow',
favoriteFood: 'Fish',
canFly: false // Not yet, but we're working on it!
}; // OK
Ми не будемо тут детально розглядати для чого потрібна така незвична фіча і особливості її використання, зробимо це в окремій статті, натомість зазначимо, що для більш менш повсякденних задач declaration merging це радше те, що може призводити до небажаних помилок, наприклад, якщо ви не помітили існуючий інтерфейс, і випадково продублювали його десь нижче в файлі, що приведе до того, що в цьому інтерфейсі будуть властивості із двох оголошенних інтерфейсів.
Мабуть варто зауважити, що на практиці зі мною такого ще не ставалося, хоча інтерфейси використовую доволі інтенсивно.
Помилки
Наступна різниця - це те що інтерфейс з extends краще обробляють помилки ніж type з intersection. Про це теж можна прочитати в документаціі Typescript > Handbook > Objects
Давайте розглянемо наступний приклад
Припустимо у нас є interface Person
interface Person {
name: string;
age: string;
}
Якщо ми спробуємо розширити інтерфейс Person, але при цьому перепишимо тип будь-якої властивості на несумісний тип, то ми отримаємо ось таку помилку
interface IPerson extends Person {
// ~~~~~~~ Interface 'IPerson' incorrectly extends interface 'Person'.
// Types of property 'age' are incompatible.
// Type 'number' is not assignable to type 'string'.
age: number;
}
Якщо аналогічну дію проробити з types i intersection - то помилки в нас не буде, натомість властивість age
отримає тип never
.
type TPerson = Person & { age: number; }; // no error, unusable type
І тут більш бажаною поведінкою буде те, як працює interface
, тому що ми швидше зможемо виявити помилку
Index signature
Наступна невелика відмінність - це те що type alias-es мають неявну індексну сигнатуру (implicit index signature), а інтерфейси - ні. Про що йде мова, краще показати на прикладі
interface Animal {
name: 'some animal'
}
declare const animal: Animal;
const handleRecord = (obj:Record<string, string>) => { }
const result = handleRecord(animal)
// ~~~~~~~
// Argument of type 'Animal' is not assignable to parameter of type 'Record<string, string>'.
// index signature for type 'string' is missing in type 'Animal'.(2345)
В данноми прикладі в handleRecord(animal)
ми отримаємо помилку
Помилка тут зʼявляється через те, що інтерфейси можуть бути розширені за допомогою declaration merging і не факт, що нові властивості Animal будуть входити в рамки Record<string,string>
Щоб це виправити, нам треба або явно додати індексну сигнатуру в Animal
, на кшталт
interface Animal {
name: 'some animal'
[key: string]: string
}
або ж просто заміити interface
на type
type Animal = {
name: 'some animal'
}
Імʼя в помилках
Ще є супер маленька відмінность в тому, як показуються помилки, але ця відмінність настільки незначна, що не факт, що ви побачити різницю з першого погляду
Подібність
Це все по відмінностям, в усьому ж іншому, все що стосується обʼектів - interface
і type
- поводяться однаково, наприклад
- Вони можуть бути дженеріками,
interface
може наслідуватиtype
- класс може реалізувпти як
interface
так іtype
.
Висновки
Cеред великих відмінностей між інтерфейсами і псевдонімами для типів є два вирішальні фактори, на яки б я звернув би увагу:
- Інтерфейси переважають в performance.
- З іншого боку в них є механізм delcaration merging, що може приводити до несподіваних помилок.
Всі ж інші відмінності на мй власний розсуд не такі суттєві.
Офіційна документація рекомендує використовувати interface до тієї пори, поки вам в конкретному випадку не знадобляться ті функції, які є у type alias, і яких бракує інтерфейсам, наприклад mapped types або union types.
For the most part, you can choose based on personal preference, and TypeScript will tell you if it needs something to be the other kind of declaration. If you would like a heuristic, use
interface
until you need to use features fromtype
.
Багато статей від різних авторів на просторах інтернету рекомендують усюди використовувати type
, щоб бути послідовним і не наражати себе на небезпеку випадкового розширення інтерфейсів за допомогою declaration merging.
Я б порекомендував робити наступним чином
-
Якщо у вас legacy проект - використовуйте те, що прийнято писати на цьому проекті.
-
Якщо це новий проект - використовуйте те, що вам більше до вподоби, тому що навіть ті “суттєві” відмінності не такі вже і суттєві.
- Якщо у вас нема вподобань, і це буде більш-менш великий проект, для якого буде важливим час компіляції Typescript-a, то віддайте перевагу interface,в іншому ж впадку використовуйте type :)
Дякую за увагу!