TypeScript: Классы vs интерфейсы
Классы и интерфейсы являются мощными структурами, которые облегчают не только объектно-ориентированное программирование, но и проверку типов в TypeScript. Класс - это объект, из которого мы можем создавать объекты с одинаковой конфигурацией - свойства и методы. Интерфейс - это группа связанных свойств и методов, которые описывают объект, но не обеспечивают их реализацию или инициализацию.
Поскольку обе эти структуры определяют, как выглядит объект, обе они могут использоваться в TypeScript для наших переменных. Решение об использовании класса или интерфейса действительно зависит от нашего варианта использования: только проверка типов, подробности реализации (обычно путем создания нового экземпляра) или даже оба! Мы можем использовать классы для проверки типов и базовой реализации, тогда как мы не можем использовать интерфейс. Понимание того, что мы можем получить от каждой структуры, позволит нам принять лучшее решение, которое улучшит наш код и опыт разработчиков.
Использование класса в TypeScript
ES6 официально представил класс в экосистеме JavaScript. TypeScript рассширяет классы JavaScript дополнительными возможностями, такими как проверка типов и статические свойства. Это также означает, что всякий раз, когда мы переносим наш код в любой целевой JavaScript-код по нашему выбору, транспортер будет хранить весь код нашего класса в переданном файле. Следовательно, классы присутствуют на всех этапах нашего кода.
Мы используем классы как объектные фабрики. Класс определяет план того, как объект должен выглядеть и действовать, а затем реализует этот план путем инициализации свойств класса и определения методов. Поэтому, когда мы создаем экземпляр класса, мы получаем объект, который имеет действенные функции и определенные свойства. Давайте рассмотрим пример определения класса с именем PizzaMaker:
class PizzaMaker { static create(event: { name: string; toppings: string[] }) { return { name: event.name, toppings: event.toppings }; } }
PizzaMaker - это простой класс. У него есть статический метод с именем create. Что делает этот метод особенным, так как мы можем использовать его, не создавая экземпляр класса. Мы просто вызываем метод в классе напрямую - так же, как мы это делаем с чем-то вроде Array.from:
const pizza = PizzaMaker.create({ name: 'Inferno', toppings: ['cheese', 'peppers'], }); console.log(pizza); // Output: { name: 'Inferno', toppings: [ 'cheese', 'peppers' ] }
Затем PizzaMaker.create() возвращает новый объект - не класс - со свойствами name и toppings, определенными из объекта, переданного ему в качестве аргумента.
Если PizzaMaker не определил create как статический метод, то для использования метода нам потребуется создать экземпляр PizzaMaker:
class PizzaMaker { create(event: { name: string; toppings: string[] }) { return { name: event.name, toppings: event.toppings }; } } const pizzaMaker = new PizzaMaker(); const pizza = pizzaMaker.create({ name: 'Inferno', toppings: ['cheese', 'peppers'], }); console.log(pizza); // Output: { name: 'Inferno', toppings: [ 'cheese', 'peppers' ] }
Мы получаем тот же результат, что и при создании, как статический метод. Возможность использования классов TypeScript с существующим экземпляром класса и без него делает их чрезвычайно универсальными и гибкими. Добавление статических свойств и методов в класс заставляет их действовать как одноэлементный, а определение нестатических свойств и методов заставляет их действовать как фабрика.
Теперь уникальным для TypeScript является возможность использовать классы для проверки типов. Давайте объявим класс, который определяет, как выглядит пицца:
class Pizza { constructor(public name: string, public toppings: string[]) {} }
В определении класса Pizza мы используем удобное сокращение TypeScript для определения свойств класса из аргументов конструктора - это экономит много времени на вводе! Pizza может создавать объекты, которые имеют свойства name и toppings:
const pizza = new Pizza('Inferno', ['cheese', 'peppers']); console.log(pizza); // Output: Pizza { name: 'Inferno', toppings: [ 'cheese', 'peppers' ] }
Помимо имени Pizza перед объектом pizza, которое показывает, что объект фактически является экземпляром класса Pizza, выходные данные новых Pizza(...) и PizzaMaker.create(...) одинаковы. Оба подхода дают объект с одинаковой структурой. Следовательно, мы можем использовать класс Pizza для проверки типа аргумента event PizzaMaker.create(...):
class Pizza { constructor(public name: string, public toppings: string[]) {} } class PizzaMaker { static create(event: Pizza) { return { name: event.name, toppings: event.toppings }; } }
Мы сделали PizzaMaker гораздо более декларативным и, следовательно, гораздо более читабельным. И не только это, но если нам нужно применить ту же структуру объектов, определенную в Pizza в других местах, у нас теперь есть переносимая конструкция для этого! Приложите экспорт к определению Pizza, и вы получите доступ к нему из любой точки вашего приложения.
Использование Pizza в качестве класса - замечательно подходит, если мы хотим определить и создать Pizza, но что, если мы хотим определить только структуру Pizza, но нам никогда не понадобится создавать ее экземпляр? Вот когда интерфейс становится удобным!
Использование интерфейса TypeScript
В отличие от классов, интерфейс представляет собой виртуальную структуру, которая существует только в контексте TypeScript. Компилятор TypeScript использует интерфейсы исключительно для проверки типов. После того, как ваш код будет скомпилирован в прод режиме, он будет удален из его интерфейсов.
И хотя класс может определять фабрику или единичный объект, обеспечивая инициализацию его свойств и реализацию его методов, интерфейс - это просто структурный контракт, который определяет, какие свойства объекта должны иметь как имя и как тип. То, как вы реализуете или инициализируете свойства, объявленные в интерфейсе, не имеет к этому отношения. Давайте посмотрим на пример, преобразовав наш класс Pizza в интерфейс Pizza:
interface Pizza { name: string; toppings: string[]; } class PizzaMaker { static create(event: Pizza) { return { name: event.name, toppings: event.toppings }; } }
Поскольку Pizza в качестве класса или интерфейса используется классом PizzaMaker исключительно для проверки типов, рефакторинг Pizza в качестве интерфейса вообще не влияет на тело класса PizzaMaker. Посмотрите, как интерфейс Pizza просто перечисляет свойства name и toppings и присваивает им тип. Также изменилось то, что мы больше не можем создавать экземпляр Pizza. Давайте далее объясним это основное различие между интерфейсом и классом, снова рассматривая Pizza как класс.
Использование класса TypeScript против использования интерфейса Typescript
На самом деле наш текущий код обеспечивает проверку типов для Pizza, но не может создать пиццу:
interface Pizza { name: string; toppings: string[]; } class PizzaMaker { static create(event: Pizza) { return { name: event.name, toppings: event.toppings }; } }
Это прискорбно, потому что мы упускаем прекрасную возможность для дальнейшего улучшения декларативного характера и читабельности нашего кода. Обратите внимание, как PizzaMaker.create() возвращает объект, который, безусловно, очень похож на Pizza! У него есть имя, которое является строкой, и его начало, которое является строковым массивом - мы выводим типы свойств из типа event, которое является объектом Pizza. Не было бы лучше, если бы мы могли вернуть экземпляр Pizza из PizzaMaker.create()?
Как упоминалось много раз ранее, мы не можем создать экземпляр интерфейса Pizza, так как это вызовет ошибку. Тем не менее, мы можем снова выполнить рефакторинг Pizza, чтобы он стал классом, а затем вернуть экземпляр Pizza:
class Pizza { constructor(public name: string, public toppings: string[]) {}; } class PizzaMaker { static create(event: Pizza) { return new Pizza(event.name, event.toppings); } } const pizza = PizzaMaker.create({ name: 'Inferno', toppings: ['cheese', 'peppers'] };
Мы реализуем структуру, которую принимает аргумент event в PizzaMaker.create(), и в то же время можем создавать объект, который определяет тип Pizza как класс! Здесь мы получаем лучшее из обоих миров - план и контракт. Это зависит от вас, что нужен для ваших случаев использования.
В заключение
Мы многому научились, не углубляясь в огромное количество кода. Если вам нужно/желательно создать экземпляр, возможно, пользовательского объекта, в то же время получая преимущества таких вещей, как проверка типов, таких как аргументы, возвращаемые типы или обобщенные элементы, - класс имеет смысл. Если вы не создаете экземпляры - у нас есть интерфейсы, и их преимущество заключается в том, что вы не генерируете никакого дополнительного кода, он позволяет нам «виртуально» проверять тип кода.
Поскольку и интерфейс, и класс определяют структуру объекта и в некоторых случаях могут использоваться взаимозаменяемо, стоит отметить, что если нам нужно разделить структурное определение между различными классами, мы можем определить эту структуру в интерфейсе, а затем для каждого класса реализовать этот интерфейс! Затем каждый класс должен будет объявить или реализовать каждое свойство интерфейса. Это сила TypeScript, и она также очень гибкая. У нас есть комплексный объектно-ориентированный дизайн в сочетании с универсальной проверкой типов.