Интерфейсы и типы TypeScript: в чём разница
- 14 апреля 2023
Помимо базовых типов, 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
включает контракт для методов walk
, run
(результат наследования) и 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
актуален во время применения объектно-ориентированного подхода, а также при создании библиотек и готовых пакетов с компонентами.
В случае с ООП всё относительно понятно: интерфейсы часть этого мира. Фраза «реализовать интерфейс» разработчику привычней по сравнению с «реализовать тип». С этим трудно поспорить. К тому же вы помните про наследование интерфейсов. Эта возможность может оказаться полезной.
Интерфейсы также пригодятся при разработке библиотеки или универсальных компонентов. В первую очередь из-за возможности бесшовного расширения. При необходимости разработчик, который применяет библиотеку, может расширить нужный интерфейс. Для этого достаточно объявить интерфейс с тем же идентификатором.
«Доктайп» — журнал о фронтенде. Читайте, слушайте и учитесь с нами.
Читать дальше
Ошибка JavaScript «Uncaught TypeError: Cannot read property of undefined». Что делать?
Описание проблемы
Эта ошибка возникает, когда вы пытаетесь получить доступ к свойству объекта, который в данный момент имеет значение undefined
. Например:
let obj = undefined;
console.log(obj.property); // Uncaught TypeError: Cannot read property 'property' of undefined
Возможные причины
- Объект не был инициализирован. Возможно, переменная еще не была объявлена или ей не присвоено значение.
- Неправильный путь к данным. Вы пытаетесь обратиться к свойству объекта, но объект отсутствует в цепочке.
- Асинхронность. Данные могли еще не загрузиться или быть доступны в момент обращения.
- Опечатка в названии свойства. Вы могли неверно написать имя свойства объекта.
Шаги по исправлению
1. Проверка объекта перед доступом к свойствам
Убедитесь, что объект существует, прежде чем пытаться получить его свойства.
if (obj !== undefined && obj !== null) {
console.log(obj.property);
}
// Или современный способ:
console.log(obj?.property); // Вернет undefined, если obj равен null или undefined
2. Инициализация объекта перед использованием
Если переменная должна содержать объект, убедитесь, что он инициализирован.
Неверный код:
let data;
console.log(data.user.name); // Ошибка
Исправление:
let data = { user: { name: "Иван" } };
console.log(data.user.name); // "Иван"
3. Проверка API или данных с сервера
Если данные загружаются с сервера, добавьте проверку, что ответ корректен.
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
if (data?.user?.name) {
console.log(data.user.name);
} else {
console.error('Данные пользователя отсутствуют');
}
});
4. Использование значений по умолчанию
Для предотвращения ошибок можно задавать значения по умолчанию.
let user = undefined;
let userName = user?.name || "Гость";
console.log(userName); // "Гость"
5. Отладка с помощью console.log
Проверяйте промежуточные значения переменных в консоли браузера.
console.log(obj); // Убедитесь, что объект определен
Пример из жизни
Вы пишете код для отображения имени пользователя из объекта user
:
let user = null; // Данные еще не загружены
console.log(user.name); // Uncaught TypeError: Cannot read property 'name' of null
Исправление:
let user = null;
console.log(user?.name || "Имя пользователя не найдено"); // "Имя пользователя не найдено"
Итог
Следуя этим рекомендациям, вы сможете избежать ошибок, связанных с доступом к свойствам undefined
или null
. Если ошибка продолжает появляться, убедитесь, что данные корректно загружаются, и используйте инструменты отладки.
- 15 ноября 2024
Как исправить ошибку JavaScript «Uncaught ReferenceError: variable is not defined»
Эта ошибка возникает, когда вы пытаетесь обратиться к переменной, которая не была объявлена в текущей области видимости. Например:
console.log(myVariable); // Uncaught ReferenceError: myVariable is not defined
Возможные причины
- Вы пытаетесь обратиться к переменной до ее объявления.
- Переменная объявлена внутри функции и недоступна за ее пределами.
- Ошибка в написании имени переменной (опечатка).
Шаги по исправлению
1. Проверка объявления переменной
Убедитесь, что переменная объявлена перед ее использованием.
Неверный код:
console.log(myVariable); // Ошибка
Исправление:
let myVariable = 10;
console.log(myVariable); // 10
2. Правильное определение области видимости переменной
Если переменная объявлена внутри функции, она недоступна за ее пределами.
Неверный код:
function myFunction() {
let localVariable = "Привет";
}
console.log(localVariable); // Ошибка
Исправление:
function myFunction() {
let localVariable = "Привет";
console.log(localVariable); // "Привет"
}
myFunction();
3. Использование глобальных переменных
Если переменная должна быть доступна везде, объявляйте ее глобально (но избегайте этого, если возможно).
let globalVariable = "Глобальная переменная";
function showVariable() {
console.log(globalVariable);
}
showVariable(); // "Глобальная переменная"
4. Проверка имени переменной на ошибки
Проверьте, нет ли опечаток в имени переменной.
Неверный код:
let myVariable = "Данные";
console.log(myVarible); // Ошибка
Исправление:
let myVariable = "Данные";
console.log(myVariable); // "Данные"
5. Использование typeof
для проверки существования переменной
Вы можете использовать оператор typeof
, чтобы проверить, объявлена ли переменная.
if (typeof myVariable !== "undefined") {
console.log(myVariable);
} else {
console.log("Переменная не определена");
}
Пример из жизни
Вы пишете функцию для проверки данных пользователя:
function checkUser() {
if (userName) {
console.log(`Добро пожаловать, ${userName}!`);
}
}
checkUser(); // Ошибка: userName не определен
Исправление:
let userName = "Иван";
function checkUser() {
if (userName) {
console.log(`Добро пожаловать, ${userName}!`);
}
}
checkUser(); // "Добро пожаловать, Иван!"
Итог
Эта ошибка часто возникает из-за опечаток или неверной области видимости. Убедитесь, что переменные объявлены и доступны в нужных местах. Если необходимо, используйте typeof
для проверки их существования.
- 15 ноября 2024
Как исправить ошибку JavaScript «Uncaught TypeError: Cannot set property of undefined»
Эта ошибка возникает, когда вы пытаетесь присвоить значение свойству объекта, который не существует (имеет значение undefined
или null
). Например:
let obj;
obj.property = "value"; // Uncaught TypeError: Cannot set property 'property' of undefined
Возможные причины
- Объект не был инициализирован.
- Вы обращаетесь к объекту, который уже удален или еще не создан.
- Ошибка в цепочке данных, например, промежуточное свойство объекта равно
undefined
.
Шаги по исправлению
1. Инициализация объекта перед использованием
Убедитесь, что объект объявлен и инициализирован перед тем, как вы присваиваете его свойствам значения. Неверный код:
let obj;
obj.property = "value"; // Ошибка
Исправление:
let obj = {};
obj.property = "value"; // Работает
2. Проверка существования объекта перед доступом
Проверяйте объект на null
или undefined
, чтобы избежать ошибок. Пример:
let obj;
if (obj) {
obj.property = "value";
} else {
console.log("Объект не определен");
}
3. Использование опциональной цепочки
Если ошибка возникает в глубоко вложенных объектах, используйте опциональную цепочку. Пример:
let obj;
obj?.nestedProperty?.subProperty = "value"; // Не вызовет ошибку
4. Проверка асинхронных данных
Если объект формируется после загрузки данных, убедитесь, что данные доступны перед их использованием.
let obj;
setTimeout(() => {
obj = { property: "value" };
console.log(obj.property); // Работает
}, 1000);
// Нельзя обращаться к obj до инициализации:
console.log(obj.property); // Ошибка
Пример из жизни
Вы загружаете данные пользователя с сервера и пытаетесь установить свойство объекта:
let user;
user.name = "Иван"; // Ошибка
Исправление:
let user = {};
user.name = "Иван"; // Работает
Итог
Чтобы избежать ошибки «Cannot set property of undefined», убедитесь, что объект существует и инициализирован. Используйте проверки на null
, опциональную цепочку или инициализируйте объект перед работой с его свойствами.
- 15 ноября 2024
Как исправить ошибку JavaScript «Uncaught SyntaxError: Unexpected token»
Эта ошибка возникает, когда интерпретатор JavaScript сталкивается с неожиданным токеном (символом) в коде, который нарушает синтаксические правила. Например:
let obj = { key: "value", }; // Uncaught SyntaxError: Unexpected token }
Возможные причины
- Лишние или пропущенные символы, такие как запятые, точки с запятой, кавычки или скобки.
- Неправильное использование синтаксиса, например, забытые конструкции или операторы.
- Смешение разных типов кавычек (например, двойных и одинарных) без соблюдения правил их вложенности.
Шаги по исправлению
1. Проверка корректности синтаксиса
Убедитесь, что все запятые, точки с запятой и кавычки находятся на своих местах. Неверный код:
let obj = { key: "value", }; // Лишняя запятая
Исправление:
let obj = { key: "value" }; // Правильный синтаксис
2. Проверка закрытия скобок
Проверьте, закрыты ли все фигурные, круглые и квадратные скобки. Неверный код:
if (true {
console.log("Ошибка!");
}
Исправление:
if (true) {
console.log("Исправлено!");
}
3. Проверка типов кавычек
Не смешивайте одинарные и двойные кавычки. Неверный код:
let str = "Привет'; // Ошибка
Исправление:
let str = "Привет"; // Или let str = 'Привет';
4. Использование отладчика
Откройте консоль браузера или отладчик в вашей IDE, чтобы найти строку с ошибкой. Она будет указана в сообщении об ошибке.
// В консоли может быть указано:
Uncaught SyntaxError: Unexpected token } at script.js:10
Исправьте строку, где произошла ошибка.
5. Проверка работы кода в строгом режиме
Добавьте "use strict";
в начало вашего файла или функции, чтобы браузер проверял ваш код на соответствие строгому синтаксису.
"use strict";
let obj = { key: "value" };
console.log(obj);
Пример из жизни
Вы пытаетесь использовать стрелочную функцию, но забыли закрыть скобки:
const add = (a, b => {
return a + b;
};
Исправление:
const add = (a, b) => {
return a + b;
};
Итог
Чтобы избежать ошибок «Unexpected token», всегда проверяйте синтаксис вашего кода на наличие лишних или пропущенных символов, используйте инструменты отладки и следите за правильным использованием конструкций JavaScript.
- 15 ноября 2024
300кк в наносекунду
Игра, где нужно забрать своё и продержаться ещё один день.
- 7 марта 2024
9 книг по JavaScript для начинающих в 2024
Все вокруг говорят, что книги — прошлый век. Но вовремя прочитанная хорошая книжка может здорово помочь в изучении нового языка или технологии, а то и вообще целиком объяснить какую-нибудь важную штуку. Например, какие бывают алгоритмы, или зачем нужен рефакторинг. К тому же, хоть фреймворки меняются каждый год, основы обычно долго не меняются.
Мы опросили знакомых разработчиков, узнали, что читают они сами, и предлагаем вам подборку хороших книг по JavaScript.
- 6 марта 2024
Объект URL в JavaScript: полный разбор
Объект 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);
- 23 января 2024
Генерация QR-кодов на JS в 4 шага. Node.js + qrcode
Давайте сделаем простой REST API на Node.js и Express, который будет генерировать QR-коды для любой ссылки. Если у вас ещё не установлены Node.js
и npm
, установите их с официального сайта.
- 22 ноября 2023
ChatGPT не справляется
Притворитесь нейросетью и решите 101 задачку по JavaScript как можно быстрее.
- 2 ноября 2023
Знакомство с JavaScript
Теперь, когда вы знаете, как создать структуру веб-страницы с помощью HTML и оформить ее стилями с помощью CSS, пришло время оживить её с помощью JavaScript (JS). JavaScript — это мощный язык программирования, который используется для создания интерактивных и динамических веб-сайтов.
Вы можете добавить JavaScript в ваш HTML-документ двумя способами:
Встроенный JavaScript: непосредственно в HTML-документ, в тегах <script>
:
<script>
alert("Привет, мир!");
</script>
Внешний JavaScript: подключение внешнего .js
файла к HTML-документу:
<script src="script.js"></script>
- 1 ноября 2023