🚀 Вам бесплатно доступен тренажёр по HTML и CSS.

Помимо базовых типов, TypeScript предоставляет разработчикам операторы для определения интерфейсов (interface) и псевдонимов для типов (type). В актуальных версиях TypeScript эти операторы во многих случаях взаимозаменяемы. Может сложиться впечатление, что кроме написания никаких различий между ними нет. На самом деле это не так. Разберёмся на примерах.

Псевдонимы типов

Оператор type позволяет определить новый псевдоним для существующего типа. Слово «псевдоним» используется неслучайно. Оператор фактически добавляет дополнительное имя для существующего типа. Новый тип данных при этом не создаётся. Взгляните на пример:

type OrderNumber = string;

Мы определили псевдоним типа OrderNumber. Фактически это дополнительное имя для типа string. При объявлении переменной типа OrderNumber, нам не придётся делать каких-то преобразований при попытке записать в неё строковое значение. Ведь OrderNumber — дополнительное имя для типа string:

const myOrderNumber: OrderNumber = '31337';

Запомните, оператор type не создаёт новый тип, а добавляет псевдоним. Мы специально делаем на этом акцент, так как в различной литературе и статьях при обсуждении оператора type используют словосочетание «новый тип». Это сделано для удобства повествования. Фактически новый тип не создаётся.

⭐ Узнайте больше о теории типов, научитесь на практике использовать аннотацию типов и обобщённое программирование на профессиональном курсе.

Взаимозаменяемость

Во многих ситуациях type и interface взаимозаменяемы. Неважно каким оператором воспользуетесь, больших отличий сразу не заметите. С одной стороны — это хорошо, с другой вводит путаницу. Рассмотрим на примере определение псевдонима типа для описания заказа в импровизированном интернет-магазине. С точки зрения кода, заказ удобно описать в виде объекта определённой формы. Воспользуемся псевдонимами типов:

type Order = {
  id: string;
  createdAt: Date;
  items: string[];
}

const myOrder: Order = {
  id: '31337',
  createdAt: new Date(),
  items: ['orange', 'banana'],
}

Сначала мы определяем псевдоним типа, а затем демонстрируем его применение.

Объект myOrder должен соответствовать структуре Order и если забыть определить какое-то свойство, компилятор сразу напомнит об этом.

Аналогичного результата можно добиться при помощи интерфейсов. Вместо оператора type воспользуемся interface:

interface Order {
  id: string;
  createdAt: Date;
  items: string[];
}

Пример, где определяется объект заказа типа Order остаётся без изменений. Получается, что одну и ту же задачу мы решили разными способами. Какому оператору отдавать предпочтение? В последних версиях TypeScript грань между операторами interface и type стала тонкой (пример выше хорошо иллюстрирует это), но разница всё же есть.

Слияние интерфейсов

Интерфейсы поддерживают декларативное слияние, а псевдонимы типов нет. Объявив два или более интерфейса с одинаковыми идентификаторами (именами), мы получим один общий интерфейс:

interface Order {
  id: string;
  createdAt: Date;
  items: string[];
}

interface Order {
  status: string;
}

interface Order {
  owner: string;
}

const myOrder: Order = {
  id: '31337',
  createdAt: new Date(),
  items: ['orange', 'banana'],
  status: 'created',
  owner: 'Michael Jackson',
}

Мы определили три интерфейса с одинаковым идентификатором (Order), а затем воспользовались им для определения нового заказа. Обратите внимание, это полностью валидный код. Никаких ошибок о дублировании идентификатора нет. TypeScript выполнил слияние и на выходе получился один интерфейс Order.

Слияние интерфейсов может показаться бессмысленной возможностью, но это не так. Лучше всего она себя проявляет при разработке библиотек. Разработчик получает возможность расширять интерфейс без внесения изменений в исходное определение. Клиенту всегда важен более гибкий интерфейс.

А что на счёт псевдонимов? Они так не умеют. При определении двух псевдонимов с одинаковыми идентификаторами (именами) возникнет ошибка: «Duplicate identifier Order».

Типы пересечения

При разработке на TypeScript часто возникает необходимость комбинировать различные типы. Один из вариантов комбинации — пересечение типов. Результатом пересечения становится тип с общими характеристиками типов, участвующих в пересечении.

Если сказать проще, при пересечении типов A и B, мы получим тип C. Ему соответствуют значения, которые одновременно принадлежат к типам A и B, то есть обладают обязательными характеристиками каждого типа.

В TypeScript эту задачу решает оператор амперсанд (&). Рассмотрим на примере.

type OrderIdentifier = {
  id: string;
}

type OrderStatus = {
  status: string;
}

type Order = OrderIdentifier & OrderStatus;

За счёт пересечения мы получаем новый тип Order. Он объединяет общие характеристики типов OrderIdentifier и OrderStatus. Получить новый тип пересечения возможно и на основании интерфейсов:

interface OrderIdentifier {
  id: string;
}

interface OrderStatus {
  status: string;
}

type Order = OrderIdentifier & OrderStatus;

Результат выполнения этого кода не отличается от предыдущего. Однако, при объединении интерфейсов мы получаем тип пересечения Order. Обратите внимание, мы получаем именно тип пересечения, а не интерфейс. Получить новый интерфейс пересечения не получится. Это ещё одно отличие между type и interface.

Типы объединения

Псевдонимы типов можно объединять. В результате объединения получается новый тип. Новый тип содержит всё что есть в типах, участвующих в объединении. Для объединения применяется оператор |. Рассмотрим на примере:

type Dog = {
  bark: () => void;
}

type Cat = {
  meow: () => void;
}

type Animal = Dog | Cat;

// Собака Гуффи
const goofy: Dog = {
  bark() {
    console.log('bark');
  }
}

// Кот Том
const tom: Cat = {
  meow() {
    console.log('meow');
  }
}


// Animal подойдёт для кошек
let animal: Animal = tom;

//  и для собак
animal = goofy;

Объединять можно не только псевдонимы, но и интерфейсы. Принцип тот же, что и при пересечении. Результатом станет новый псевдоним типа, объединяющий интерфейсы. Получить новый интерфейс в результате объединения не получится.

Интерфейсы и классы

Интерфейсы особенно удобны при использовании объектно-ориентированного подхода. Сначала проектируется интерфейс, а потом классы, которые его имплементируют. Для этого в TypeScript есть отдельная синтаксическая конструкция implements. Рассмотрим на примере:

interface Cat {
  meow: () => void;
}

class Tiger implements Cat {
  meow() {
    console.log('meow-meow-bark');
  }
}

Мы описали интерфейс Cat, а затем определили класс Tiger. Этот класс реализует интерфейс Cat. Обратите внимание на ключевое слово implements. Приведённый пример кода ещё одна демонстрация, что интерфейс можно заменить на псевдоним типа. Например, так:

type Cat = {
  meow: () => void;
}

Классы могут имплементировать псевдоним типа, поэтому пример с определением класса Tiger остаётся актуальным, а мы опять увидели взаимозаменяемость type и interface.

Пожалуй, можно переходить к следующему разделу, но у интерфейсов есть ещё один козырь в рукаве. Интерфейсы поддерживают наследование. Работает это точно так же, как и в классах. При наследовании интерфейсов применяется оператор extends:

interface Cat {
  meow: () => void;
}

interface FastCat extends Cat {
  run: () => void;
}

class Tom implements FastCat {
  meow() {
    console.log('meow');
  }

  run() {
    console.log('run');
  }
}

Интерфейс FastCat наследуется от интерфейса Cat. Таким образом, интерфейс FastCat включает всё, что есть в определении Cat. При имплементации интерфейса FastCat, класс должен реализовать оба метода meow и run.

Это ещё не всё. В качестве родителя для интерфейса может выступать класс. Новый интерфейс будет содержать поля и методы класса, а также то, что разработчик добавит в интерфейс. Наследуя интерфейс от класса, помните, что класс в этом случае не должен содержать приватных полей. Рассмотрим пример:

class Animal {
 walk() {
   console.log('walk');
 }

 run() {
   console.log('run');
 }
}

// Интерфейс включает методы
// класса Animal
interface Cat extends Animal {
  meow: () => void;
}

class HomeCat implements Cat {
  walk() {
    console.log('Cat can walk');
  }

  run() {
    console.log('Cat can run');
  }

  meow() {
    console.log('meow');
  }
}

Рассматривать пример следует с описания класса Animal. Обычный класс с двумя методами: walk и run. Затем мы определяем новый интерфейс Cat. Он наследуется от класса Animal, следовательно, новый интерфейс Cat включает контракт для методов walkrun (результат наследования) и meow.

Для проверки интерфейса определяем новый класс HomeCat. Он имплементирует интерфейс Cat, который в свою очередь наследуется от класса Animal. Это означает, что класс HomeCat должен имлементировать все три метода.

Псевдонимы типов не поддерживают наследования. Оно возможно только с интерфейсами. Это ещё одно различие между этими конструкциями.

Кортежи

TypeScript поддерживает кортежи. Кортеж — упорядоченный набор фиксированной длины. Кортежи похожи на массивы, даже синтаксис используется такой же. Но в отличии от последних, кортеж не может динамически расширяться, и типы значений, а также их количество известны заранее. В следующем примере объявляем новый кортеж Developer:

type Developer = [string, string, number];
const ivan: Developer = ['Ivan', 'Ivanov', 33];

Объявить новый кортеж с помощью interface нельзя. Это ещё одна ситуация, когда операторы не взаимозаменяемы. Стоит добавить, что внутри интерфейса определять кортежи можно:

interface Developer {
  name: string;
  top2skill: [string, string];
}

const ivan: Developer = {
  name: 'Ivan',
  top2skill: ['run', 'sing'],
}

Резюме

Во многих случаях операторы type и interface взаимозаменяемы. Мы убедились в этом на практике. Мы рассмотрели несколько ситуаций, когда поведение отличается и использование одного оператора вместо другого невозможно. А что делать с ситуациями, когда допустимы оба оператора?

Дать объективный ответ на этот вопрос сложно. Всё зависит от нескольких «но» и взглядов разработчика. Уместны оба варианта. Однако, мы рекомендуем по умолчанию применять type. Оператор interface актуален во время применения объектно-ориентированного подхода, а также при создании библиотек и готовых пакетов с компонентами.

В случае с ООП всё относительно понятно: интерфейсы часть этого мира. Фраза «реализовать интерфейс» разработчику привычней по сравнению с «реализовать тип». С этим трудно поспорить. К тому же вы помните про наследование интерфейсов. Эта возможность может оказаться полезной.

Интерфейсы также пригодятся при разработке библиотеки или универсальных компонентов. В первую очередь из-за возможности бесшовного расширения. При необходимости разработчик, который применяет библиотеку, может расширить нужный интерфейс. Для этого достаточно объявить интерфейс с тем же идентификатором.


«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.

ТелеграмПодкастБесплатные учебники

Читать дальше

9 книг по JavaScript для начинающих в 2024

9 книг по JavaScript для начинающих в 2024

🚀 Вам бесплатно доступен тренажёр по HTML и CSS.

Все вокруг говорят, что книги — прошлый век. Но вовремя прочитанная хорошая книжка может здорово помочь в изучении нового языка или технологии, а то и вообще целиком объяснить какую-нибудь важную штуку. Например, какие бывают алгоритмы, или зачем нужен рефакторинг. К тому же, хоть фреймворки меняются каждый год, основы обычно долго не меняются.

Мы опросили знакомых разработчиков, узнали, что читают они сами, и предлагаем вам подборку хороших книг по JavaScript.

Читать дальше
JS
  • 6 марта 2024
Объект URL в JavaScript: полный разбор

Объект URL в JavaScript: полный разбор

🚀 Вам бесплатно доступен тренажёр по HTML и CSS.

Объект URL в JavaScript представляет URL-адрес и предоставляет удобные методы для работы с ним. Он позволяет анализировать, конструировать и декодировать URL-адреса.

Создать объект URL можно двумя способами:

Конструктор URL() — самый распространённый способ, в котором вы передаёте любой URL в виде строки в качестве аргумента.

const url = new URL("https://www.example.com/path?query=123#hash");

Использование window.location — это глобальный объект в браузерах, который содержит информацию о текущем URL.

const currentUrl = new URL(window.location.href);
Читать дальше
JS
  • 23 января 2024
Генерация QR-кодов на JS в 4 шага. Node.js + qrcode

Генерация QR-кодов на JS в 4 шага. Node.js + qrcode

🚀 Вам бесплатно доступен тренажёр по HTML и CSS.

Давайте сделаем простой REST API на Node.js и Express, который будет генерировать QR-коды для любой ссылки. Если у вас ещё не установлены Node.js и npm, установите их с официального сайта.

Читать дальше
JS
  • 22 ноября 2023
Знакомство с JavaScript

Знакомство с JavaScript

🚀 Вам бесплатно доступен тренажёр по HTML и CSS.

Теперь, когда вы знаете, как создать структуру веб-страницы с помощью HTML и оформить ее стилями с помощью CSS, пришло время оживить её с помощью JavaScript (JS). JavaScript — это мощный язык программирования, который используется для создания интерактивных и динамических веб-сайтов.

Вы можете добавить JavaScript в ваш HTML-документ двумя способами:

Встроенный JavaScript: непосредственно в HTML-документ, в тегах <script>:

<script>
  alert("Привет, мир!");
</script>

Внешний JavaScript: подключение внешнего .js файла к HTML-документу:

<script src="script.js"></script>
Читать дальше
JS
  • 1 ноября 2023
Событие onclick в JS на примерах

Событие onclick в JS на примерах

🚀 Вам бесплатно доступен тренажёр по HTML и CSS.

Интерактивность — ключевой компонент любого современного сайта. И одним из наиболее часто используемых событий для создания интерактивности является событие onclick. В этой статье мы подробно разберёмся, что такое событие onclick, как его использовать и приведем примеры применения.

Событие onclick — это событие JavaScript, которое активируется, когда пользователь кликает на определенный элемент страницы. Это может быть кнопка, ссылка, изображение или любой другой элемент, на который можно нажать.

Читать дальше
JS
  • 30 октября 2023
Как перевернуть сайт. Самая короткая инструкция

Как перевернуть сайт. Самая короткая инструкция

Не представляем, зачем это может понадобиться, но не могли пройти мимо.

Никакой магии. Мы вызываем JavaScript-функцию rotateBody(), которая применяет свойство transform с значением rotate(180deg) к элементу <body>. Когда вы нажмете на кнопку «Перевернуть», всё, что находится внутри <body> будет повернуто на 180 градусов (то есть, встанет вниз головой)

function rotateBody() {
  document.body.style.transform = 'rotate(180deg)';
}

<button onclick="rotateBody()">Перевернуть</button>

Но такой код повернёт страницу только один раз. Если нужно, чтобы она возвращалась обратно при втором клике, усложним код:

let isRotated = false;

function rotateBody() {
  if (isRotated) {
    document.body.style.transform = 'rotate(0deg)';
    document.body.style.direction = "ltr";
  } else {
    document.body.style.transform = 'rotate(180deg)';
    document.body.style.direction = "rtl";
  }
  isRotated = !isRotated;
}

Надеемся, вы прочитали это описание до того, как нажать на кнопку.

JS
  • 25 октября 2023
Как узнать геолокацию: Geolocation API

Как узнать геолокацию: Geolocation API

🚀 Вам бесплатно доступен тренажёр по HTML и CSS.

Geolocation API позволяет сайтам запрашивать, а пользователям предоставлять свое местоположение веб-приложениям. Геолокация может использоваться для выбора города в интернет-магазине, отображения пользователя на карте или навигации в ближайший гипермаркет.

Основной метод Geolocation API — getCurrentPosition(), но есть и другие методы и свойства, которые могут пригодиться.

Читать дальше
JS
  • 16 октября 2023
Что такое localStorage и как им пользоваться

Что такое localStorage и как им пользоваться

🚀 Вам бесплатно доступен тренажёр по HTML и CSS.

localStorage — это место в браузере пользователя, в котором сайты могут сохранять разные данные. Это как ящик для хранения вещей, которые не исчезнут, даже если вы выключите компьютер или закроете браузер.

До localStorage разработчики часто использовали cookies, но они были не очень удобны: мало места и постоянная передача данных туда-сюда. LocalStorage появился, чтобы сделать процесс более простым и эффективным.

Читать дальше
JS
  • 12 октября 2023