Разница между типом и интерфейсом в TypeScript
Тип и интерфейс в TypeScript часто вызывают путаницу по причине поверхностной схожести. Ситуацию усугубляют устаревшие статьи, необъективные сравнения и style-guid'ы некоторых фреймворков. Например, в Angular по умолчанию включено tslint правило interface-over-type-literal, которое требует использовать интерфейсы вместо типов везде, где возможно. В этой статье разберём разницу между типом и интерфейсом в TypeScript и определимся что же использовать.
Сходства
Интерфейсы и типы могут использоваться для описания структур данных:
type Employee = { salary: number; } // Или interface Employee { salary: number; }
Могут использоваться для типизации функции:
interface CalculateSalary { (employee: Employee): number; } // Или type CalculateSalary = (employee: Employee) => number;
Интерфейсы и типы могут быть имплементированы классами:
type Employee = { giveEstimate(task: Task): number; } // Или interface Employee { giveEstimate(task: Task): number; } class YoungDeveloper implements Employee { giveEstimate(task: Task): number { return task.complexity * 0.01; } } class MatureDeveloper implements Employee { giveEstimate(task: Task): number { return task.complexity * random(10, 1000) * Math.PI; } }
Интерфейсы и типы позволяют выразить пересечение типов:
type TwitterProfile = Photographer & Musician & Entrepreneur & CoffeeDrinker; // Или interface TwitterProfile extends Photographer, Musician, Entrepreneur, CoffeeDrinker {};
Отличие 1 - Mapped типы
Интерфейсы нельзя комбинировать с mapped типами (Required, Pick, Readonly, Partial и прочими):
// С типом работает type RealProfile = Pick<TwitterProfile, 'drinkCoffee'>; // С интерфейсом не работает interface RealProfile extends Pick<TwitterProfile, 'drinkCoffee'> {};
Интерфейс может расширять только интерфейсы, классы или другие типы, поэтому код нужно переписать так:
type OnlyDrinksCoffee = Pick<TwitterProfile, 'drinkCoffee'>; interface RealProfile extends OnlyDrinksCoffee {}
По этой же причине только с помощью типа можно требовать, чтобы все свойства были обязательны или наоборот опциональны:
type TraineeDeveloper = Partial<{ salary: number; sleep: boolean; eat: boolean; }> // Может существовать без ЗП, еды и сна const trainee: TraineeDeveloper = {}
Отличие 2 - Union
Интерфейсы позволяют выразить пересечение типов, однако не позволяют выразить объединение. Пример ограничения на типах, которое нереализуемо на интерфейсах:
type Wish = | { fast: true, quality: true, cheap: false } // Дорого | { fast: true, quality: false, cheap: true } // Некачественно | { fast: false, quality: true, cheap: true } // Медленно const wish: Wish = { fast: true, quality: true, cheap: true } // Не компилируется
Ещё интерфейсы не могут расширять union типы.
Отличие 3 - Declaration merging
Интерфейсы поддерживают declaration merging - слияние интерфейсов с одинаковыми именами:
interface Employee { salary: number; } interface Employee { age: number; } const employee: Employee = { age: 23 }; // Ошибка компиляции, так как не выдали salary
Эту особенность можно использовать если тайпинги сторонней библиотеки устарели, а вам нужно расширить интерфейс недостающими свойствами и методами. Если вы делаете свою библиотеку, то тут вам решать, стоит ли давать такие точки расширения функциональности. Если библиотека на TypeScript - то не стоит, так как декларации типов на выходе всегда будут актуальными и пользователю не понадобится исправлять расхождения между типами и рантаймом. Сейчас всё больше библиотек пишутся сразу на TypeScript или поставляются с тайпингами, поэтому необходимость использовать declaration merging возникает всё реже. Вдобавок исправить в устаревшем интерфейсе можно далеко не всё - нельзя удалить свойство или поменять тип существующего:
interface Employee { salary: number; } interface Employee { salary?: number; } const employee: Employee = {}; // Ошибка, Employee всё ещё требует salary
Отличие 4 - Рекурсивные типы
До TypeScript 3.7 были отличия в том, как работают рекурсивные типы и интерфейсы. С помощью типов нельзя было типизировать рекурсивные структуры, однако в новых версиях языка проблемы нет. Подробности в release notes языка.
Отличие 5 - Совместимость с типом Record
Из-за того, что interface поддерживает declaration merging (может быть расширен в любом месте) его нельзя использовать там, где ожидается Record<string, any>
. Это может быть проблемой в ситуациях, где нужно использовать URLSearchParams или другое браузерное API, ожидающее Record<string, any>
interface Employee { name: string; } const employee: Employee = { name: '' } new URLSearchParams(employee);
Этот код не скомпилируется с интерфейсом, зато будет работать если интерфейс поменять на тип.
Итог
Типы являются более предпочтительным вариантом, так как вы можете заменить интерфейсы типами, но не наоборот. Для использования продвинутой функциональности TypeScript - mapped типов, объединения типов и условных типов интерфейсы не подойдут.