Разница между типом и интерфейсом в TypeScript
Тип и интерфейс в TypeScript часто вызывают путаницу по причине поверхностной схожести. Ситуацию усугубляют устаревшие статьи, необъективные сравнения и style-guid’ы некоторых фреймворков. Например, в Angular по умолчанию включено tslint правило interface-over-type-literal , которое требует использовать интерфейсы вместо типов везде, где возможно. В этой статье разберём разницу между типом и интерфейсом в TypeScript и определимся что же использовать.
Сходства
Интерфейсы и типы могут использоваться для описания структур данных:
type Employee = < salary: number; >// Или interface Employee
Могут использоваться для типизации функции:
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; // С интерфейсом не работает interface RealProfile extends Pick <>;
Интерфейс может расширять только интерфейсы, классы или другие типы, поэтому код нужно переписать так:
type OnlyDrinksCoffee = Pick; interface RealProfile extends OnlyDrinksCoffee <>
По этой же причине только с помощью типа можно требовать, чтобы все свойства были обязательны или наоборот опциональны:
type TraineeDeveloper = Partial> // Может существовать без ЗП, еды и сна 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 . Это может быть проблемой в ситуациях, где нужно использовать URLSearchParams или другое браузерное API, ожидающее Record
interface Employee < name: string; >const employee: Employee = < name: '' >new URLSearchParams(employee);
Этот код не скомпилируется с интерфейсом, зато будет работать если интерфейс поменять на тип.
Итог
Типы являются более предпочтительным вариантом, так как вы можете заменить интерфейсы типами, но не наоборот. Для использования продвинутой функциональности TypeScript — mapped типов, объединения типов и условных типов интерфейсы не подойдут.
Примитивные типы
Псевдонимы типов могут определять примитивные типы (символ, булевое значение, строку, число, bigint и т. д.), в то время как интерфейсы не могут.
Обратите внимание на то, что псевдоним типа не создает новых типов (отсюда и его название). Интерфейсы, напротив, всегда создают новые типы.
Типы-объединения
type Fruit = ‘apple’ | ‘lemon’;type Vegetable = ‘potato’ | ‘tomato’;// ‘apple’ | ‘lemon’ | ‘potato’ | ‘tomato’type Food = Fruit | Vegetable;const apple: Food = ‘apple’;>
Типы-объединения также можно определить только с помощью псевдонимов типов.
Типы-кортежи
type Animal = [name: string, age: number];const cat: Animal = [‘’, 1];>
Типы-кортежи также можно определить только с помощью псевдонимов типов.
Типы объектов/функций
И интерфейс, и тип могут объявлять типы объектов или типы функций. Однако интерфейс может объявлять один и тот же интерфейс несколько раз, и они будут объединены автоматически, в то время как псевдонимы типов не объединяются и должны быть уникальными.
// При использовании интерфейсов одинаковые интерфейсы автоматически объединяются.interface PrintName (name: string): void;>interface PrintName (name: number): void;>// ✅const printName: PrintName = (name: number | string) => console.log(‘name: ‘, name);>;>// При использовании псевдонимов типов они должны быть уникальными, для пересечения можно использовать только `&`.type PrintName = ((name: string) => void) & ((name: number) => void);// ✅const printName: PrintName = (name: number | string) => console.log(‘name: ‘, name);>;>
Другим ключевым моментом является то, что псевдонимы типов используют пересечение, а интерфейсы — наследование.
interface Parent printName: (name: number) => void;>// ❌ Интерфейс 'Child' некорректно расширяет интерфейс 'Parent'.interface Child extends Parent printName: (name: string) => void;>>type Parent = printName: (name: number) => void;>;type Child = Parent & // Здесь два printName будут пересекаться.// Это эквивалентно`(name: number | string) => void`printName: (name: string) => void;>;const test: Child = printName: (name: number | string) => console.log(‘name: ‘, name);>,>;test.printName(1);test.printName(‘1’);>
Как показано в ошибке выше, когда интерфейс наследуется, подтип не может конфликтовать с супертипом, его можно только расширить:
interface Parent printName: (name: number) => void;>interface Child extends Parent // ✅printName: (name: string | number) => void;>>
Интерфейс использует extends для реализации наследования, а псевдоним типа — & для реализации пересечения.
В некоторых случаях автоматическое объединение и расширение интерфейсов может пригодиться. Если вы создадите стороннюю библиотеку и откроете публичные API, пользователи смогут расширять ее через механизм интерфейсов!
Поэтому следуйте такому совету: если хотите объявить тип объекта, используйте сначала интерфейс, а при необходимости — псевдонимы типов.
Сопоставленные типы объектов
type Vegetable = ‘potato’ | ‘tomato’;type VegetableOption = [Property in Vegetable]: boolean;>;const option: VegetableOption = potato: true,tomato: false,>;// “potato” | “tomato”type VegetableAlias = keyof VegetableOption;>interface VegetableOption // ❌ Сопоставленный тип не может объявлять свойства или методы.[Property in Vegetable]: boolean;>>export <>;
Сопоставленные типы объектов могут быть определены только с помощью псевдонимов типов, включая ключевые слова in и keyof .
Неизвестные типы
const potato = < name: ‘potato’, weight: 1 >;// тип Vegetable = // имя: string;// вес: number;// >type Vegetable = typeof potato;const tomato: Vegetable = name: ‘tomato’,weight: 2,>;>
При работе с неизвестными типами можно применять typeof для захвата типа, но в этом случае допустимы только псевдонимы типов, а не интерфейсы.
Подводя итог, можно сказать, что псевдонимы типов охватывают почти все возможности интерфейсов. Однако интерфейсы всегда расширяемы, а псевдонимы типов — нет. Поэтому приходится делать выбор между ними в зависимости от конкретного случая.
- Замечательные новые фичи TypeScript 3.5
- Индексация строк в Rust и TypeScript в сравнениях
- 7 инструментов для разработки веб-компонентов
TypeScript: Сравнение Типа и Интерфейса
TypeScript, мощная надстройка над JavaScript, завоевал популярность среди разработчиков благодаря сложной системе типизации. Он предлагает два способа определения форм данных: типы и интерфейсы. Но как сделать выбор между ними? Знание тонких различий и вариантов использования очень важно для эффективной разработки. В статье мы рассмотрим типы и интерфейсы, изучим их ключевые различия и практические случаи использования, чтобы вы могли принять взвешенное решение о том, когда использовать каждый из них.
Ключевые моменты
- Понимание различий между типами и интерфейсами TypeScript очень важно для принятия обоснованных решений при разработке.
- Типы предпочтительнее использовать для примитивных значений, типов объединения/пересечения, функций и кортежей, в то время как интерфейсы обеспечивают лучшие сообщения об ошибках и производительность проверки типов.
- Выбор между ними должен основываться на личных предпочтениях, удобстве чтения и требованиях проекта.
Понимание типа и интерфейса TypeScript
В TypeScript для определения форм данных можно использовать либо типы, либо интерфейсы. Оба варианта имеют свои уникальные особенности, и понимание их различий является основополагающим для принятия обоснованного решения. Если типы используются для определения типов данных переменных, то интерфейсы определяют синтаксис классов, свойств, методов и событий.
Интерфейсы могут быть расширены дополнительными функциями после их первоначального объявления, что отличает их от типов и псевдонимов/алиасов типов в TypeScript.
Типы TypeScript
Типы TypeScript используются для определения типов данных переменных, включая встроенные типы, типы, определяемые пользователем, и дополнительные возможности, такие как псевдонимы типов. Основными типами в TypeScript являются string , boolean и number .
В TypeScript псевдоним типа позволяет изменить обозначение типа, не определяя новый тип. Объединённые типы могут быть объявлены только с помощью ключевого слова type . Такой подход предоставляет уникальные возможности, недоступные в других языках, особенно при работе с интерфейсами псевдонимов типов.
С другой стороны, типы кортежей позволяют объявить массив с фиксированным количеством элементов, причём каждый элемент имеет свой собственный тип данных. Интересно, что кортежи могут быть объявлены с помощью типов только в TypeScript.
Интерфейсы TypeScript
Интерфейсы в TypeScript представляют собой контракт, определяющий требования, которым должен удовлетворять объект. Они определяют синтаксис, которого должен придерживаться каждый объект. Компонентами интерфейса в TypeScript являются его свойства, методы и события.
Оптимальные процедуры выбора между типами и интерфейсами в TypeScript включают учёт личных предпочтений и удобочитаемости, оценку контекста и осмысление эффекта производительности.
Ключевые различия между типами и интерфейсами
Хотя типы и интерфейсы имеют ряд общих черт, между ними существуют и ключевые различия, например:
- работа с примитивными типами
- объединение и пересечение типов
- слияния объявлений
- типы функции и кортежа
Осознание этих различий необходимо для выбора подходящего инструмента для работы и оптимизации разработки на TypeScript.
Примитивные типы
Одним из ключевых различий между типами и интерфейсами является работа с примитивными типами. Типы могут использоваться с примитивными типами, такими как тип string , в то время как интерфейсы не могут использоваться с примитивными типами.
Псевдоним примитивного значения можно получить только с помощью типов, поэтому они являются рекомендуемым выбором для определения примитивных типов.
Объединение и пересечение типов
Ещё одно различие между типами и интерфейсами заключается в том, как они относятся к объединению и пересечению типов. Объединение типов может быть объявлено только с помощью ключевого слова type , а ключевое слово interface в данном контексте не применяется. Такой подход обеспечивает уникальные возможности, недоступные в других языках. Пересечение типов, напротив, создаётся с помощью оператора and в TypeScript.
Хотя интерфейсы могут комбинироваться, образуя объединение типов, они не поддерживают пересечение типов, что делает объединение типов более универсальным и выразительным в подобных ситуациях.
Слияние и расширение объявлений
Слияние объявлений — ключевое различие между интерфейсами и типами. Интерфейсы с одинаковыми именами не должны быть объявлены в одной области видимости. Это приводит к слиянию их объявлений, что может привести к неожиданным ошибкам.
Однако псевдонимы типов объединяют свойства, не приводя к ошибкам. Слияние объявлений может быть полезно для расширения определений типов сторонних библиотек, что делает интерфейсы ценным инструментом в таких сценариях.
Типы функции и кортежа
И типы, и интерфейсы могут разграничивать типы функций, но для наглядности и расширенных возможностей, таких как условные и объединительные типы, предпочтение отдаётся типам. Кортежи могут быть определены только с помощью ключевого слова type .
Как и в случае с примитивными типами, типы имеют преимущество перед интерфейсами при определении типов функций и кортежей, благодаря своей большей гибкости и выразительности.
Практические случаи использования типов и интерфейсов
Типы и интерфейсы имеют практическое применение в различных аспектах разработки на TypeScript. В зависимости от конкретной ситуации один из них может быть более подходящим, чем другой.
Мы рассмотрим некоторые часто встречающиеся случаи использования, включая объектно-ориентированное программирование, сложные структуры данных, интеграцию сторонних библиотек, и определим, что лучше подходит — типы или интерфейсы.
Объектно-ориентированное программирование
В объектно-ориентированном программировании интерфейсы лучше подходят благодаря их способности расширять классы и поддерживать наследование. Это делает интерфейсы идеальным выбором при работе с парадигмами объектно-ориентированного программирования, поскольку они обеспечивают более структурированный подход и согласованность производных классов.
Комплексные структуры данных
При работе со сложными структурами данных, благодаря поддержке объединений, пересечений и кортежей, множественные типы, в том числе объектные, часто оказываются более гибкими и выразительными. Они позволяют разработчикам создавать сложные и многократно используемые структуры данных, которые могут адаптироваться к различным сценариям. С помощью сопоставленных типов эта гибкость ещё более повышается, позволяя осуществлять ещё более мощные манипуляции с типами.
Хотя интерфейсы могут использоваться и для более простых структур данных и обеспечивают повышенную удобочитаемость и удобство сопровождения, типы, как правило, являются предпочтительным выбором для более сложных структур данных.
Интеграция со сторонними библиотеками
Интерфейсы полезны для интеграции со сторонними библиотеками благодаря возможности слияния объявлений, о чем говорилось ранее в разделе Слияние и расширение объявлений . Эта возможность позволяет разработчикам настраивать определение типа сторонней библиотеки в соответствии с требованиями конкретного проекта.
При работе со сторонними библиотеками интерфейсы могут служить мощным средством обеспечения безопасности типов, сокращения времени разработки и повышения читаемости кода.
Учёт производительности и обработки ошибок
Важными аспектами при выборе между типами и интерфейсами являются производительность и обработка ошибок. Хотя влияние использования типов на производительность по сравнению с интерфейсами минимально, интерфейсы обеспечивают более качественные сообщения об ошибках и проверку типов благодаря своей способности обнаруживать конфликты и выбрасывать ошибки при расширении.
Интерфейсы также являются более гибкими, чем типы, поскольку определяющие интерфейс элементы могут быть расширены и изменены без нарушения функциональности интерфейса.
Проверка типа и сообщения об ошибках
Интерфейсы обнаруживают конфликты и выдают ошибки при расширении, в то время как псевдонимы типов объединяют свойства без ошибок. Такое поведение позволяет улучшить сообщения об ошибках при проверке типов с помощью интерфейсов, поскольку они могут точно определить потенциальные проблемы с интерфейсом или псевдонимом типа.
В отличие от этого, псевдонимы типов могут не уловить некоторые конфликты, что может привести к неожиданному поведению и затруднить выявление первопричины проблемы.
Влияние на производительность
Несмотря на отсутствие заметной разницы в производительности между типами и интерфейсами в TypeScript, стоит отметить, что интерфейсы могут обеспечить более быструю проверку типов благодаря кэшированию по имени. Это преимущество в производительности может быть несущественным в небольших проектах, но оно может иметь значение в больших кодовых базах TypeScript со сложной иерархией типов.
Интерфейсы также могут обеспечить более организованный способ определения типов, поскольку их можно группировать.
Лучшие практики выбора между типами и интерфейсами
Выбор между типами и интерфейсами в конечном итоге зависит от личных предпочтений, удобства чтения и оценки конкретного случая использования. Хотя существуют некоторые общие рекомендации и лучшие практики, окончательное решение должно основываться на уникальных требованиях вашего TypeScript-проекта и предпочтениях вашей команды разработчиков.
Личные предпочтения и удобочитаемость
При выборе типов и интерфейсов разработчики должны учитывать свои личные предпочтения и читаемость кода. Мнения о том, какой из них более читабелен или элегантен, могут быть различными, в зависимости от индивидуальных особенностей и практики кодирования.
Очень важно найти баланс между личными предпочтениями и потребностями проекта, чтобы выбранный подход обеспечивал наилучшую читаемость и удобство сопровождения для всей команды.
Оценка ситуации
Перед выбором типа или интерфейса необходимо провести тщательную оценку ситуации и сценариев использования. Объектно-ориентированное программирование, сложные структуры данных, интеграция библиотек сторонних разработчиков — все это факторы, которые необходимо учитывать при принятии решения, включая выбор подходящего типа объекта.
Рассмотрев эти факторы и оценив плюсы и минусы каждого подхода, разработчики смогут сделать осознанный выбор, наиболее полно отвечающий потребностям их TypeScript-проекта.
Подводя итоги
В заключение следует отметить, что типы и интерфейсы TypeScript — это мощные инструменты, помогающие разработчикам создавать чистый, эффективный и поддерживаемый код. Несмотря на некоторое сходство, их ключевые различия и варианты использования делают каждый из них более подходящим для конкретных сценариев. Понимая эти различия и применяя лучшие практики, разработчики смогут использовать весь потенциал системы типов TypeScript, что приведёт к созданию более надёжных и масштабируемых проектов.
Приступая к изучению TypeScript, помните об этих различиях и выбирайте подходящий инструмент для работы. Воспользуйтесь возможностями системы типов TypeScript и позвольте ей поднять ваш опыт разработки на новую высоту.
Часто задаваемые вопросы
Что лучше использовать в TypeScript — интерфейс или тип
Интерфейсы следует использовать для слияния деклараций и объектно-ориентированного стиля наследования. Типы лучше подходят для работы с функциями и сложными типами.
В чем разница между типом и интерфейсом в TypeScript
Типы в TypeScript более гибкие и могут определять примитивы, пересечения, объединения, кортежи или различные типы данных, а интерфейсы используются для описания формы объекта. Типы используют ключевое слово type для создания нового типа, а интерфейсы — ключевое слово interface для объявления интерфейса.
Когда нужно использовать интерфейс в TypeScript
Интерфейсы в TypeScript следует использовать для проверки структуры объекта, передачи объектов в качестве параметров и создания объекта. Это целесообразно, поскольку интерфейсы обеспечивают безопасность типов, что облегчает отладку и читаемость.
Могут ли типы работать с примитивными типами
Да, типы могут использоваться с примитивными типами, что делает их полезным инструментом для структурирования данных. Однако интерфейсы не могут быть использованы подобным образом.
TypeScript: Об интерфейсах
В этом уроке мы поговорим об интерфейсах. Мы узнаем, что это такое, зачем они нужны, и в каких случаях их стоит использовать вместо типов.
Что такое интерфейс
Интерфейс — это конструкция языка TypeScript, которая используется, чтобы описывать объекты и функции.
Рассмотрим следующий пример:
interface IUser < firstName: string; pointsCount: number; >const user: IUser = < firstName: 'Mark', pointsCount: 100, >;
В данном фрагменте мы создали интерфейс и реализовали на его основе объект user .
Интерфейс выглядит как определение объектного типа. Объектные типы и интерфейсы взаимозаменяемы почти во всех ситуациях. Сравним с примером выше:
type User = < firstName: string; pointsCount: number; >const user: User = < firstName: 'Mark', pointsCount: 100, >;
Здесь мы реализовали такой же объект, но уже на основе типа, а не интерфейса. Разницы почти нет.
Документация TypeScript говорит о том, что мы можем выбирать, что использовать — тип или интерфейс. Выбор зависит от ситуации. В таком случае возникает вопрос, зачем нужны интерфейсы, когда уже есть типы?
Когда использовать интерфейсы
Интерфейсы и типы во многом похожи. Но есть отличия, на основе которых мы и можем выбирать, что именно следует использовать в конкретном случае.
Главная особенность интерфейсов связана с классами. Классы, которые реализуют интерфейсы, содержат внутри себя свойства и методы, указанные в реализуемом интерфейсе:
interface Countable < count(): number; >class SchoolClass implements Countable < // Тут какая-то логика count(): number < // Обязательно создать этот метод, так как он указан в интерфейсе >> const sc = new SchoolClass(); // Возвращает число студентов в классе sc.count();
В этом примере мы реализовали класс на основе интерфейса. Теперь во всех функциях, где объекты используются только для того, чтобы посчитать количество чего-либо внутри них, можно указывать Countable вместо SchoolClass :
// А не function doSomething(obj: SchoolClass) function doSomething(obj: Countable) < // Где-то внутри вызывается obj.count(); >
Так благодаря интерфейсам функция становится более универсальной. Мы можем передать любые объекты, соответствующие Countable , а не только SchoolClass . В программировании такая возможность называется полиморфизмом подтипов (Subtyping).
Задание
Вам дан интерфейс IVehicle. Задача состоит в том, чтобы на основе этого интерфейса реализовать класс Car, который будет иметь метод calcFuelNeeded, принимающий расстояние в километрах и возвращающий расход топлива на указанную дистанцию. Также у класса Car должна быть функция-конструктор, которая принимает и реализует свойства, указанные в интерфейсе.
const porche = new Car(4, 'red', true, 20); console.log(porche.calcFuelNeeded(200)); // 40
Упражнение не проходит проверку — что делать?
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя
Это нормально , в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.