Меня зовут Дима. Я Frontend разработчик в компании fuse8. Работая с TypeScript, рано или поздно сталкиваешься с вопросом: что выбрать — типы или интерфейсы? В нашей команде мы активно используем TypeScript, уделяя особое внимание типам. В статье я хотел бы поделиться особенностями работы с типами и интерфейсами, которые могут быть полезны в вашей практике.
Основные отличия типов и интерфейсов
Типы используются для задания именованных типов данных, включая примитивы, объекты, функции и массивы. Они позволяют объединять или пересекать типы и поддерживают использование ключевых слов typeof, keyof при присвоении.
Интерфейсы служат для описания структуры объектов. Интерфейсы поддерживают декларативное объединение и могут быть расширены другими интерфейсами или классами.
И типы, и интерфейсы позволяют описывать структуры данных в TypeScript, что помогает предотвратить ошибки на этапе компиляции и делать код более предсказуемым.
Для примитивов и кортежей используйте типы
Создать строковый, числовой или другой примитивный тип с помощью интерфейса просто не получится.
Пример с примитивами:
type UserId = string;
type ColumnHeight = number;
type isActive = boolean;
В интерфейсах примитивные типы можно использовать в описании свойств объектов:
interface User {
id: string;
age: number;
isActive: boolean;
}
Пример с кортежем:
type Coordinates = [number, number];
Добиться похожего поведения можно и с помощью интерфейса, но так не рекомендуется делать:
interface Coordinates {
0: number;
1: number;
length: 2; // фиксированная длина
}
Интерфейсы с одинаковыми именами объединяются
Интерфейсы обладают особенностью, которая отсутствует у типов: если у вас есть несколько интерфейсов с одинаковыми именами, они могут объединяться. Это особенно полезно, когда вы работаете с внешними библиотеками или проектами, где структуру объекта нужно расширять.
Рассмотрим пример:
interface User {
id: number;
}
interface User {
name: string;
}
const user: User = {
id: 100,
name: 'John Doe'
};
В этом примере два интерфейса User
сливаются в один, который содержит оба свойства: id
и name
. Это позволяет гибко добавлять новые поля к уже существующим структурам, не трогая оригинальный код. Если бы вы пытались сделать то же самое с типами, TypeScript выдал бы ошибку — названия типов должны быть уникальными, даже если типы находилсь находились бы в разных файлах.
Объединение происходит не на уровне одного файла, а на уровне всего проекта. Поэтому важно помнить, особенно, если проект большой, что есть возможность случайно расширить уже существующий интерфейс. Также это правило работает для предустановленных интерфейсов, например, если нужно затипизировать комментарий с помощью интерфейса, выбрав название Comment
, то мы расширим интерфейс Comment
, который находится в lib.dom.d.ts.
Для большего погружения можно ознакомиться с документацией по объединению интерфейсов.
Типы можно пересекать и объединять, интерфейсы – наследовать
Пересечение типов осуществляется с помощью оператора &
:
type User = { id: string; };
type Article = { title: string; };
type UserArticle = User & Article;
Здесь UserArticle
объединяет свойства как пользователя, так и статьи.
Похожего поведения в интерфейсах можно добиться с помощью ключевого слова extends
:
interface User {
id: string;
}
interface Article {
title: string;
}
interface UserArticle extends User, Article {}
Но это не одно и тоже, extends
используется только для интерфейсов и подразумевает наследование, тогда как пересечение типов с помощью &
может использоваться как для интерфейсов, так и для любых других типов.
Существует мнение, что наследование интерфейсов работает быстрее, чем пересечение типов. Это связано с тем, что операции расширения требуют меньше ресурсов на этапе компиляции, чем пересечения типов. В гайде по производительности TypeScript также рекомендуется отдавать предпочтение наследованию интерфейсам, если важна скорость компиляции.
Однако реальные тесты показывают, что разница незначительна. Например, проверка 10 тысяч одинаковых конструкций для интерфейсов и типов не выявила существенной разницы в скорости компиляции. Эксперимент можно найти здесь.
Другое отличие заключается в том, что если оба типа являются объектами, и в этих объектах содержатся поля с одинаковыми названиями, но разными типами, то extends
выдаст ошибку, а при использовании&
ошибки не будет. Рассмотрим пример:
type User = {
id: string;
}
type Article = {
id: number;
}
type UserArticle = User & Article;
В UserArticle
ошибки нет, но id
имеет тип never
, так как id
не может быть одновременно и строкой и числом. А при использовании extends
получаем ошибку:
Типы также поддерживают объединение с помощью оператора |
. Это удобно, когда тип может быть один из нескольких вариантов:
type User = {
id: string;
}
interface Article {
title: string;
}
type ProductId = string;
type Payload = User | Article | ProductId;
Лаконичность типов при использовании Utility Types
Типы имеют более более лаконичный синтаксис при использовании Utility Types, чем интерфейсы. Например, для создания типа с необязательными полями можно воспользоваться утилитой Partial.
Вот как это выглядит для типов:
type User = {
id: string;
}
type UserPartial = Partial<User>;
Теперь давайте посмотрим, как это будет выглядеть с интерфейсом:
interface User {
id: string;
}
interface UserPartial extends Partial<User> {}
В случае с интерфейсом нам приходится добавлять дополнительные конструкции extends и пустые фигурные скобки {}, что делает код менее читабельным. Это не критично, но может добавлять лишний «шум», особенно если часто используются такие утилиты как Partial, Pick, Omit и другие.
Свойства интерфейсов сохраняют источник
Ещё одна интересная особенность интерфейсов заключается в том, что их свойства сохраняют информацию о том, откуда они были взяты. Это может быть полезно при отладке кода.
Пример:
interface User {
id: string;
}
interface Article {
name: string;
}
interface UserArticle extends User, Article {};
const userArticle: UserArticle = {
id: 'test',
name: 'test'
};
Если вы посмотрите на объект userArticle
, поле id
будет связано с User.id: string
, а name — с Article.name: string
. Это может помочь лучше понять, откуда взято конкретное свойство при сложных наследованиях.
Теперь давайте перепишем тот же пример на типах:
type User = {
id: string;
}
type Article = {
name: string;
}
type UserArticle = User & Article;
const userArticle: UserArticle = {
id: 'test',
name: 'test'
};
В случае с типами при отладке оба поля id
и name
будут просто строками (string), и информация о том, откуда они взяты, будет потеряна.
Когда использовать типы, а когда интерфейсы?
Можно взять за основу правило: использовать типы по умолчанию, а интерфейсы, когда это необходимо.
Использование интерфейсов можно рассмотреть в библиотеках, которые будут ставиться в проекты, чтобы дать возможность расширить типы при необходимости. Либо в проектах, которые используют подход ООП.
Полезные ссылки:
Комментарии (23)
NeoCode
21.09.2024 03:39+1Какой интересный язык! Кажется в работе с типами он продвинулся дальше многих других современных языков.
Кстати получается что пересечение & это по сути теоретико-множественное объединение, а объединение | это "тип-сумма" (tagged union, variant). А вот интересно, можно в нем сделать теоретико-множественное пересечение, разность или симметрическую разность типов?
artptr86
21.09.2024 03:39
meonsou
21.09.2024 03:39+1Объединение это не тип-сумма. Tagged union это дизъюнктивное объединение из теории множеств, а union это обычное объединение. Ну а & это пересечение, а не объединение.
NeoCode
21.09.2024 03:39ИМХО в теории множеств вообще нет аналога для Tagged union. Потому что в tagged union нельзя хранить разные элементы одновременно, а в теории множеств про "одновременность" вообще ничего не говорится.
По поводу пересечения & - вот пример из интернета
type User = { name: string; age: number; }; type Employee = { name: string; department: string; salary: number; }; type CommonUserEmployee = User & Employee; // Result: { name: string; age: number; department: string; salary: number; }
т.е. на выходе мы имеем тип, объединяющий поля из двух типов, причем одинаковые поля сливаются - в точности как в объединении множеств. A={1,2}, B={2,3}, A∪B={1,2,3}.
meonsou
21.09.2024 03:39Там уже ниже написали, типы рассматриваются как множества значений. Пересечение типов это пересечение множеств их значений. Точно так же тип-сумма называется суммой потому что количество его значений это сумма количеств значений входящих в него типов (как у дизъюнктивного объединения), а у типа-произведения, соответственно, произведение.
Про разные элементы одновременно не понял что вообще имелось в виду.
NeoCode
21.09.2024 03:39Там уже ниже написали, типы рассматриваются как множества значений
Тогда понятно
а у типа-произведения, соответственно, произведение.
Т.е. здесь имеется в виду декартово произведение
Про разные элементы одновременно не понял что вообще имелось в виду
Я просто пытался рассматривать типы как множества полей структур, а не как множества значений.
iliazeus
21.09.2024 03:39A & B
- это объединение известных утверждений про типы A и B (вида "у этого типа есть поле А), но пересечение множеств значений этих типов.Разность множеств значений можно выразить через встроенный тип
Exclude<A, B>
. Симметрическая разность - это, соответственно,Exclude<A | B, A & B>
.
iliazeus
21.09.2024 03:39Кажется в работе с типами он продвинулся дальше многих других современных языков.
Насколько понимаю, это было вынужденно - нужно было натянуть статические типы на очень динамические интерфейсы уже существующих JS-библиотек.
devunion
21.09.2024 03:39Теоретически широкие возможности -- это хорошо. А на практике в результате такой код получается, что начинаешь думать, что лучше бы этих возможностей не было .
Akuma
21.09.2024 03:39+1Вообще просто не использую интерфейсы. Ну ли о так редко, что даже не помню.
Просто незачем, типы все перекрывают, удобно «наследуются», есть partial, а больше ничего и не нужно обычно.
19Zb84
Интерфейсы нужны для взамодействия пользователя с кодом. Как любой сайт - это интерфейс программы.
Типы это описание передаваемых данных в коде.
Пример. Посмотрите структуру документации libp2p
Mingun
Ваше описание слишком расплывчато. Что значит «взаимодействие пользователя с кодом»? Пользователь у нас кто? Программист. И что же за взаимодействие с кодом может быть у программиста? Создание/изменение структур данных и функций, их обрабатывающих. А для этого эти самые структуры надо как-то описать. Вот и пришли к вашему определению типов.
С другой стороны, «описание передаваемых данных» само по себе в вакууме не нужно. Оно есть для того, чтобы мы понимали, что с этими данными можно делать, а чего нельзя. А это — работа с данными. Какого рода работа? Преобразование, передача в функции, возврат из функций. Всё это есть взаимодействие с кодом. Приходим к вашему описанию интерфейсов.
Получается, типы и интерфейсы — одно и то же?
19Zb84
Что бы описать интерфейс вы должны понимать приложение, которое описывается.
Если вы не понимаете что описываете вы не сможете составить интрефейс.
По этому я не люблю использовать тайп скрипт у себя в приложениях. Тайп скрипт это инструмент для описания проектов, которые уже имеют определенную структуру.
libp2p про которую я написал как раз уже за много лет определились с этим. У них очень небольшое колличества документации. Все сделанно через typescript docs
Вот пример интерфейса. Он говорит, как я взаимодействую с библиотекой.
Они могли бы описать через типы, но тогда я не смог бы понять на что я смотрю.
Интерфейс это тот объект который я физически составляю. (пишу. Точки входа в библиотеку. Как розетка )
Типы это то что я использую для составления интерфейса.
Так же как веб интерфейс, эо то что я физически делаю, а для составления его я использую апи запросы.
Ну вообще да. Можно одно через другое описать.
https://libp2p.github.io/js-libp2p/modules/libp2p.index.html
Mingun
Непонятно, что поменяется для вас, как для пользователя библиотеки, если вместо
interface Libp2pInit<T> {
будетtype Libp2pInit<T> = {
.19Zb84
А что для вас поменяется ?
Mingun
Для меня — пока ничего, но я как раз и пытаюсь выяснить, что мне следует использовать в своих проектах на TypeScript —
type
илиinterface
? Потому что пока ясности нет, зачем нужны оба, делают вроде одно и тоже, просто чуть по-разному.19Zb84
Если вы говорите о небольшом проекте, то там typeScript вообще не нужен. Это графоманство. имхо.
Если это сложный проект как эта библиотека, то это сильно влияет на потраченное время.
Я либо потрачу 10 минут на поиски информации либо часа 4.
Ваш подход я не понимаю, при том что вы что то пишете. Подход тех, кто пишет библиотеку я понимаю. И мне легко ей сейчас пользоваться при том, что ниодного слова от них я не слышал.
Это основное что ля меня важно.
Mingun
Не понял, почему с типами вы потратите 4 часа на поиск информации, если с интерфейсами вы тратите на это 10 минут?
Mingun
Проект действительно небольшой, но типизация даже в нём была бы полезна. Ну и вообще, он в том числе затевался с целью потрогать этот ваш TypeScript «в условиях, приближенных к боевым».
gsaw
Я раньше так же думал, JS хватает, какие проблемы, если я знаю, что я возвращаю из своей функции, к чему описывать типы. Потом поработал в проекте с Typenscript и по другому уже не могу.
На прошлой неделе переводил свой старый, относительно небольшой проект с JS на TS. В некоторых местах даже удивлялся, как оно вообще работало. :) Там такие смешные ошибки были в коде, которые без типов были просто не видны. Просто опечатки, пропущенные параметры.
Да и вообще, когда просто ide делает подсказки уже ускоряет работу с кодом. Тот же GitHub Copilot лучше ориентируется в коде. Нет нужды открывать файл с функцией, что бы понять, что за структуру она возвращает.
Ну и заставляет лучше структурировать код, а не генерить какие то структуры в разных местах разные.
Короче выигрыш от описания типов больше, чем затраты на описание типов.
meonsou
Даже если в конкретном случае не поменяется ничего, как минимум кодовая база станет однороднее, а значит понятнее. Вот есть 2 почти одинаковых инструмента с редкими практическими различиями, я предпочитаю по этим различиям и проводить границу. Либо использовать интерфейсы всегда, кроме случаев когда нужны типы, либо наоборот. Когда начинаются разговоры про "взаимодействие пользователя с кодом" и "описание данных" это уже какая-то субъективная ерунда безосновательная. Вот как, например, новый разработчик в вашей команде поймёт когда ему использовать что? Вы можете сформулировать чёткое и однозначное определение своих терминов? А если и можете, будет ли оно проще и будет ли эта сложность того стоить?
Что касается выбора между "всегда типы пока не нужны интерфейсы" и "всегда интерфейсы пока не нужны типы" (где граница предельно ясна), я предпочитаю первое, потому что типы работают проще и интуитивнее. С интерфейсами могут потом ещё возникнуть проблемы и окажется что надо переписывать на типы, с типами такое случается реже.
iliazeus
Я понимаю, что по заголовку статьи это не так понятно, но в ней идет речь именно про ключевые слова
type
иinterface
в TypeScript.