Когда речь заходит о TypeScript, один из самых частых вопросов, которые мне задают студенты, звучит так: «Что лучше использовать: интерфейсы или типы?»
На эту тему написано уже множество статей, в том числе на Хабре (например тут и тут), и обсуждений в сообществе более чем достаточно. Однако, даже после всех этих разъяснений часто остаётся ощущение, что однозначного ответа нет. Одни авторы говорят: «Интерфейсы лучше для декларативности», другие уверяют: «Типы универсальнее», и каждый подкрепляет свою точку зрения примерами.
Долгое время я придерживался простой и прагматичной позиции: «В целом разницы нет, всё зависит от предпочтений и соглашений команды». Однако, чтобы не быть голословным и действительно разобраться в нюансах, я решил покопаться в этом вопросе глубже.
И как результат, я не только нашёл интересные аспекты использования интерфейсов и типов, но и захотел поделиться ими с вами. Эта статья — попытка дать максимально объективный взгляд на выбор между интерфейсами и типами, рассматривая их с точки зрения практики, возможностей и ограничений TypeScript.
Если вас интересует, какой из подходов лучше подходит для вашего проекта, или вы хотите более чётко понимать разницу между ними, то давайте разберёмся вместе!
В поисках наиболее точного и понятного объяснения различий между интерфейсами и типами я пересмотрел множество источников. Среди них особенно выделилась статья «Type vs Interface» от Mykyta M.. В ней автор обстоятельно разбирает преимущества и недостатки обоих подходов, а также приводит примеры их использования.
Эта статья вдохновила меня на более глубокое исследование, а также помогла систематизировать мои знания. Однако, несмотря на подробный разбор, вопросы всё равно остались. Например, что лучше использовать для сложных типов? Какие из подходов проще масштабировать и поддерживать в долгосрочной перспективе?
Поэтому в этой статье я решил обобщить прочитанный материал и дополнить его своими наблюдениями и выводами. Мы разберём, где интерфейсы действительно лучше, чем типы, а где типы показывают свои сильные стороны. Кроме того, я покажу примеры реальных сценариев из моей практики, где один подход оказался предпочтительнее другого.
Перед тем как углубляться в сравнение, давайте освежим в памяти, что такое типы (types) и интерфейсы (interfaces) в TypeScript. Мы напомним основы их синтаксиса и использования, чтобы создать общую базу для дальнейшего обсуждения. Этот раздел будет особенно полезен новичкам, для которых TypeScript может быть ещё незнакомой территорией.
Интерфейсы (Interfaces)
Интерфейсы в TypeScript используются для описания структуры объектов. Они определяют, какие свойства и методы должны быть у объекта. Интерфейсы легко расширяются (extends) и предназначены для декларативного подхода.
Пример интерфейса:
interface User {
id: number;
name: string;
email?: string; // Необязательное свойство
}
const user: User = {
id: 1,
name: "Alice",
};
Свойство
email
является необязательным, что указано с помощью?
.Интерфейсы могут быть расширены для добавления новых свойств:
interface Admin extends User {
role: string;
}
const admin: Admin = {
id: 1,
name: "Alice",
role: "superadmin",
};
Пример интерфейса для функции:
Интерфейсы также позволяют типизировать функции, задавая их сигнатуру.
interface CalculateArea {
(width: number, height: number): number;
}
const calculateArea: CalculateArea = (width, height) => {
return width * height;
};
const area = calculateArea(5, 10); // 50
console.log(`The area is ${area}`);
Типы (Types)
Типы (или алиасы типов) предоставляют более универсальный инструмент для работы с типизацией. TypeScript предоставляет широкие возможности для работы с типами, включая литералы, примитивы, кортежи, объединения, отображаемые типы и многое другое. Давайте рассмотрим примеры использования различных конструкций, которые делают работу с типами гибкой и мощной.
Литеральные типы (Literal Types)
Литеральные типы ограничивают значение переменной конкретным значением. Это полезно для создания строгих ограничений.
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type StatusCode = 200 | 400 | 404 | 500;
type MagicWord = "Abracadabra";
const method: HttpMethod = "POST"; // допустимо
const status: StatusCode = 200; // допустимо
const magic: MagicWord = "Abracadabra"; // допустимо
Примитивные типы (Primitive Types)
Вы можете явно указать типы примитивов, таких как строки, числа и логические значения.
type UserName = string;
type IsLoggedIn = boolean;
type UserId = number;
const userName: UserName = "Alice";
const isLoggedIn: IsLoggedIn = true;
const userId: UserId = 123;
Кортежи (Tuples)
Кортежи позволяют определять массивы с фиксированной длиной и типами для каждого элемента.
type Coordinates = [x: number, y: number];
type ApiResponse = [status: StatusCode, message: string];
const point: Coordinates = [10, 20];
const response: ApiResponse = [200, "Success"];
Объекты (Objects)
Подобно интерфейсам типы позволяют описывать сложные структуры объектов.
type Product = {
id: number;
name: string;
price: number;
categories: string[];
stock?: number; // Необязательное свойство
};
const product: Product = {
id: 101,
name: "Laptop",
price: 1200,
categories: ["Electronics", "Computers"],
};
Объединения (Union Types)
Объединение типов позволяет создать новый тип, который может быть любым из указанных типов. Это полезно, когда функция или переменная могут принимать несколько разных типов значений. Например, функция, которая принимает либо строку, либо число, и работает с обоими случаями:
function formatInput(input: string | number): string {
if (typeof input === "string") {
return `You entered a string: "${input}"`;
} else {
return `You entered a number: ${input}`;
}
}
Пересечения (Intersection)
Подобно интерфейсам типы позволяют определять новый тип, расширяющий существующий с помощью пересечения:
type Admin = User & {
role: string;
};
const admin: Admin = {
id: 3,
name: "Charlie",
role: "moderator",
};
Отображаемые типы (Mapped Types)
Отображаемые типы позволяют создавать новые типы на основе существующих, модифицируя их свойства.
type ReadOnly<T> = { readonly [P in keyof T]: T[P] };
type User = {
id: number;
name: string;
email: string;
};
type ReadOnlyUser = ReadOnly<User>;
const user: ReadOnlyUser = {
id: 1,
name: "Alice",
email: "alice@example.com",
};
// user.id = 2; // Ошибка: Свойство только для чтения
Функции (Functions)
Типы позволяют описывать сигнатуры функций.
type PerformAction = (action: "Run" | "Jump" | "Attack", target: string) => void;
const performAction: PerformAction = (action, target) => {
console.log(`${action} performed on ${target}`);
};
performAction("Run", "enemy");
Фактически выбор между интерфейсами и типами встает только в случае типизации объектов или функций, т.к. для всего остального типы - это оптимальный выбор.
Если говорить о функциях, большинство разработчиков выбирают типы. Это более прозрачный и удобочитаемый способ для типизации.
Таким образом, нам нужно ответить только на вопрос что предпочтительнее для типизации объектов.
Производительность
Разобравшись с синтаксисом, давайте рассмотрим более существенные различия, начиная с тех, которые можно найти в официальной документации.
Using interfaces with
extends
can often be more performant for the compiler than type aliases with intersections
Итак, в документации сказано, что использование расширений интерфейсов дает большую производительность, чем пересечение типов. Важно понимать, что мы не говорим о производительности веб-приложения или сайта. Это касается только процесса разработки (так называемый developer experience - DX) и разговор только о скорости компиляции тайпскрипт кода. Негативный эффект производительности сказывается только на времени, необходимом для проверки типизации и создания артефактов (например файлов .js и .d.ts).
Если у вас массивный проект и есть необходимость часто осуществлять сборку кода, то производительность действительно может играть большую роль. Например, в этом проекте время компиляции 10 000 интерфейсов с использованием extends сравнивалось со временем компиляции 10 000 пересечений типов. В результате вариант с интерфейсами занял 1 минуту 26 секунд 629 миллисекунд, а вариант с пересечениями — 2 минуты 33 секунды 117 миллисекунд. Разница довольно существенная.
Declaration Merging
Ещё одно интересное различие между типами и интерфейсами — это так называемое объединение объявлений или Declaration Merging, которое характерно только для интерфейсов. В базовом примере это работает следующим образом: если вы объявляете интерфейсы с одинаковыми именами несколько раз, TypeScript автоматически объединит их в один интерфейс.
interface Vehicle {
make: string;
model: string;
}
interface Vehicle {
wheels: number;
isElectric: boolean;
}
const bike: Vehicle = {
make: "Yamaha",
model: "MT-07",
wheels: 2,
isElectric: false,
};
const tesla: Vehicle = {
make: "Tesla",
model: "Model 3",
wheels: 4,
isElectric: true,
};
Объединение интерфейсов часто используется в библиотеках или фреймворках, где можно расширить существующий интерфейс (например, Window
или HTMLElement
) для добавления новых свойств, не изменяя исходный код. Однако, следует помнить что это также может приводить к неожиданному поведению, если вы случайно объявили один и тот же интерфейс дважды.
Обработка ошибок
Другая неочевидная разница между типами и интерфейсами в обработке ошибок. В официальной документации этому посвящен специальный раздел.
Чтобы увидеть разницу, давайте рассмотрим следующий пример.
interface Person {
name: string;
age: string;
}
interface IPerson extends Person {
age: number;
}
В примере выше мы пытаемся переопределить тип переменной age
и такое действие вызовет ошибку совместимости типов.
При похожем случае, но с использованием типов мы не получим ошибку, а тип переменной age
станет never
, что является абсолютно бесполезным.
type TPerson = Person & { age: number; }; // нет ошибки, для age тип never
Index Signature
Ещё одно незначительное отличие заключается в том, что псевдонимы типов имеют неявную сигнатуру индекса, а интерфейсы — нет. Это лучше показать на примере:
interface Animal {
name: 'some animal'
}
declare const animal: Animal;
const handleRecord = (obj: Record<string, string>) => { }
const result = handleRecord(animal)
В этом примере handleRecord(animal)
выдаст нам сообщение об ошибке.
Ошибка возникает из-за того, что интерфейсы могут быть расширены путём объединения объявлений, и нет гарантии, что новые свойства Animal
будут соответствовать Record<string,string>
.
Чтобы исправить это, нам нужно либо явно добавить сигнатуру индекса в Animal
или использовать тип вместо интерфейса:
interface Animal {
name: 'some animal'
[key: string]: string
}
type Animal = {
name: 'some animal'
}
Сообщения об ошибках
Имена интерфейсов всегда будут отображаться в сообщениях об ошибках в исходном виде, но только если они используются по имени. Вы можете посмотреть пример из официальной документации по ссылке.
Выводы
Таким образом, можно выделить два основных различия между типами и интерфейсами:
Интерфейсы имеют преимущество с точки зрения производительности.
Интерфейсы имеют механизм слияния объявлений, который может быть полезным, но при этом приводить к неожиданным ошибкам.
Все остальные различия, на мой взгляд, не столь существенны.
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
для единообразия и во избежание риска случайного расширения интерфейсов с помощью объединения объявлений.
Я рекомендую следующий подход:
Если вы работаете над существующим проектом, придерживайтесь уже принятого стиля — используйте то, что активно применяется в этом проекте.
Если вы начинаете новый проект, выбирайте то, что вам больше по душе. Даже "значительные" различия между типами и интерфейсами на практике не столь критичны.
Если у вас нет явных предпочтений и проект предполагается достаточно крупным, где время компиляции TypeScript имеет значение, отдавайте предпочтение интерфейсам. В остальных случаях можете спокойно использовать типы.
В общем, мои изначальные советы студентам действительно оказались верными: в большинстве случаев выбор между интерфейсами и типами сводится к предпочтениям, соглашениям команды и специфике проекта. TypeScript предоставляет мощные инструменты для обоих подходов, и большинство различий становятся важными только в специфических ситуациях.
Спасибо за внимание!
Надеюсь, эта статья помогла вам лучше разобраться в различиях между типами и интерфейсами в TypeScript. Если у вас остались вопросы или вы хотите обсудить тему более подробно, пишите в комментариях — с радостью отвечу!
Если вам интересны темы веб-разработки, TypeScript, JavaScript, и вы хотите получать больше полезного контента, присоединяйтесь к моему Telegram-каналу: https://t.me/+w_2X35bZ4wFkOTQy.
В канале я делюсь опытом, разбираю интересные кейсы, обсуждаю новости индустрии и просто создаю ламповую атмосферу для разработчиков. Буду рад видеть вас там! ?
Комментарии (4)
gen1lee
03.01.2025 10:46Всегда используйте типы, и никогда интерфейсы. Все. Лишние символы в языке это мусор, от которого нужно избавляться.
dom1n1k
03.01.2025 10:46Интерфейсы нужны, чтобы описывать логически связанные группы свойств, которые не тянут на самодостаточный тип в терминах бизнес-логики.
И лучше по возможности собирать классы из нескольких интерфейсов, чем наоборот потом расковыривать переусложненные типы при помощи Partial, Pick, Omit и прочей трудночитаемой бесовщины.Например, объект
SomeObject
содержит 50 полей, но для перемещения его по плоскости нам нужно знать его координатыx, y
и угол поворотаphi
. Выделяем их в интерфейс типаIPosition
- и если у наc есть условная функцияmove
, то она ожидает в качестве параметра именноIPosition
, а неSomeObject
потому что остальные 47 полей ей знать ни к чему.
artptr86
В случае с описанием типа через
type
, он тоже будет отображаться в сообщениях об ошибках при использовании по имени:Впрочем, это логично. TS может использовать имя типа, если этот тип явно поименован.
Таким образом, здесь
interface
иtype
не отличаются.maks88sgt Автор
Согласен, что разница тут ускользающая и тип действительно будет отображаться при обращении по имени. Однако это упоминается в официальной документации. Вот два скриншота со страницы с примером обратите внимание на строку после описания ошибки