Класы и Интерфейсы в TypeScript

Dec 30, 2017

В JavaScript с переходом на ES6 нам стали доступны классы, но в TypeScript классы существуют еще дольше. В TypeScript однако нам также доступна концепция interface и очень часто при добавлении аннотации типов к определенным частям нашего кода возникает вопрос:

Должен ли я использовать интерфейс или класс для аннотации этого типа?

В этой статье мы рассмотрим и сравним интерфейсы с классами в TypeScript, так что мы сможем ответить на этот вопрос.

Пример

Давайте начнем с примера, чтобы сосредоточится на том, что мы попытаемся понять в этом посте:

fetch('https://test/test/api')
.then((response) => {
    console.log(response.status) //  HTTP статус, например, 200
})

Рассмотрим тривиальную задачу при конструировании UI - получаем данные с удаленного сервера, и затем используем эти данные в нашем frontend коде.

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

На этом этапе, чтобы повысить безопасность нашей программы, мы хотели бы добавить нашу собственную аннотацию типа к response, чтобы явно сообщить компилятору TypeScript о том, каким типом должен быть ответ.

// 'Response' будет определен здесь...

fetch('https://test/test/api')
.then((response: Response) => {
    console.log(response.status)
})

Теперь мы подошли к центральному вопросу, который мотивировал этот пост ... Должен ли наш тип Response быть определен как интерфейс или как класс?

Что такое interface?

Когда TypeScript проверяет типы различных частей нашей программы одним из ключевых подходов данной проверки является так называемая "утиная типизация".

"Если это похоже на утку и квакает как утка, значит это утка."

Другими словами мы определяем может ли что-то быть классифицировано как определенный тип, взглянув на его соответствие требуемым характеристикам/структуре/форме. Давайте обозначим это как 'форма' с этого момента.

В TypeScript interface это путь для нас, по которому мы можем взять конкретную форму и задать ей имя, таким образом, что позднее мы сможем ссылаться на нее как на тип в нашей программе.

Давайте возьмем утиную аналогию и создадим для нее интерфейс:

// A duck must have...
interface Duck {
    // ...a `hasWings` свойство со значением `true` (тип boolean)
    hasWings: true
    // ...a `noOfFeet` свойство со значением `2` (тип number)
    noOfFeet: 2
    // ...a `quack` метод, который ничего не возвращает
    quack(): void
}

Далее в нашем TypeScript коде, если мы хотим быть уверенными, что что-либо является уткой (что на самом деле означает: он "реализует наш Duck интерфейс"), все что нам потребуется это указать его тип как Duck.

// Это пройдет проверку типу
const duck: Duck = {
    hasWings: true,
    noOfFeet: 2,
    quack() {
        console.log('Quack!')
    },
}

// Это не пройдет проверку по типу
// так как некорректно реализован Duck интерфейс
const notADuck: Duck = {}
// TypeScript скажет нам:
// "Type '{}' is not assignable to type 'Duck'.
// Property 'hasWings' is missing in type '{}'."

Теперь мы понимаем, как интерфейсы могут помочь TypeScript поймать больше потенциальных проблем в нашем коде на этапе компиляции, но существует одна критическая особенность интерфейсов, которую мы должны иметь ввиду.

interface используются TypeScript только на этапе компиляции и затем удаляются. Интерфейсы не попадают в наш финальный JavaScript код.

Давайте завершим раздел об интерфейсах, определив наш мертвый простой тип Response как interface:

interface Response {
    status: number // некоторый HTTP статус, такой как 200
}

fetch('https://test/test/api')
.then((response: Response) => {
    console.log(response.status)
})

Если мы пропустим этот код через TypeScript компилятор, мы получим программу, которая компилируется без ошибок и JavaScript на выходе может быть таким:

fetch('https://test/test/api')
.then(function (response) {
    console.log(response.status);
});

Мы можем видеть, что наша дополнительная информация о типе во время компиляции не повлияла на нашу программу во время работы. Вот она - магия интерфейсов в TypeScript.

Используем классы

Давайте рассмотрим наш пример и переопределим наш тип Response как class вместо interface:

class Response {
    status: number // // некоторый HTTP статус, такой как 200
}

fetch('https://test/test/api')
.then((response: Response) => {
    console.log(response.status)
})

В этом случаем мы просто поменяли interface на class и если мы передадим наш код TypeScript компилятору мы по-прежнему будем иметь тот же уровень безопасности типов и компиляция пройдет без ошибок. Таким образом на данный момент поведение идентично.

Реальное отличие можно увидеть, когда мы рассматриваем наш скомпилированный JavaScript

Подобно interface, class также являются JavaScript конструкцией, но class представляют собой нечто большее, чем просто именованный фрагмент с информацией о типе.

Самая большая разница между class и interface в том, что class обеспечивают реализацию чего-либо, а не только его формы.

Вот наш скомпилированный код, после того как мы поменяли interface на class:

var Response = (function () {
    function Response() {
    }
    return Response;
}());
fetch('https://test/test/api')
.then(function (response) {
    console.log(response.status);
});

Огромная разница. Как мы видим, наш класс сконвертирован в функцию, форма которой совместима с ES5, при этом данная функция является ненужной частью нашего финального JavaScript приложения. Если бы у нас было большое приложение, в котором для описания аннотаций типов использовались бы классы, то скачиваемый пользователями код мог бы быть слишком раздутым.

Заключение

Если мы хотим создавать типы для данных модели пришедших с удаленного сервере или другого подобного ресурса, то хорошая идея начать использовать interface.

В отличие от class, interface полностью удаляются на этапе компиляции и предотвращают раздутие финального JavaScript кода.

При необходимости не составит труда переделать interface в class.

По мотивам Typescript: class против interface

Добавить комментарий