Как стать автором
Обновить

TypeScript для бэкенд-разработки

Время на прочтение 9 мин
Количество просмотров 13K
Автор оригинала: CHRISTOPH BURNICKI
Язык Java по-прежнему правит бал в backend-разработке. На то немало причин: быстрота, безопасность (если, конечно, закрыть глаза на null-указатели), плюс обширная, хорошо протестированная экосистема. Но в эру микросервисов и гибкой разработки стали важнее и другие факторы. В некоторых системах бывает не обязательно держать пиковую производительность и располагать надежной экосистемой стабильных зависимостей, если речь идет о простеньком сервисе, выполняющем CRUD-операции и преобразование данных. Более того, многие системы приходится оперативно строить и перестраивать, чтобы идти в ногу с быстрой итеративной разработкой фич.

Не составляет труда разработать и развернуть простой сервис Java, благодаря всепроникающей магии Spring Boot. Но, поскольку замкнутые классы приходится тестировать, а данные -–преобразовывать, в коде в изобилии появляются строители, преобразователи, конструкторы перечислений и сериализаторы, и всем этим вымощен ужасный путь в ад стереотипного кода Java. Именно поэтому часто задерживается разработка новых фич. И, да, генерация кода работает, но это не слишком гибкий прием.

TypeScript пока не успел хорошо зарекомендовать себя среди бэкенд-разработчиков. Вероятно, потому что известен как набор декларативных файлов, позволяющих добавить в JavaScript некоторую типизацию. Но, все же, есть масса логики, на представление которой ушли бы десятки строк на Java, и которую можно представить всего в нескольких строках TypeScript.
Масса фич, о которых говорят как о характерных чертах TypeScript, на самом деле относятся к JavaScript. Но TypeScript также можно рассматривать как самостоятельный язык, обладающий определенным синтаксическим и концептуальным сходством с JavaScript. Поэтому давайте ненадолго отвлечемся от JavaScript и рассмотрим TypeScript сам по себе: это красивый язык с исключительно мощной, но при этом гибкой системой типов, с кучей синтаксического сахара и, наконец, с нуль-безопасностью!

Мы разместили на Github репозиторий со специально разработанным веб-приложением на Node/TypeScript, а также с некоторыми дополнительными объяснениями. Там есть также ветка продвинутой сложности с примером луковой архитектуры и более нетривиальными концепциями из области типизации.

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


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

Поскольку TypeScript компилируется в обычный JavaScript, в качестве среды исполнения для бэкенда используется Node.js. В отсутствие всеобъемлющего фреймворка, который напоминал бы Spring, имеем, что типичный веб-сервис будет использовать более гибкий фреймворк, служащий веб-сервером (отличный пример такого рода — Express.js). Следовательно, он получится менее «магическим», а его базовая настройка и конфигурирование будут устроены более явно. В таком случае сравнительно сложные сервисы также потребуют сильнее повозиться с настройкой. С другой стороны, настройка сравнительно мелких приложений не составляет труда, причем, осуществима практически без предварительного изучения фреймворка.

Управление зависимостями без труда осуществляется при помощи гибкого, но при этом мощного менеджера пакетов Node, npm.

Основы


При определении классов поддерживаются модификаторы контроля доступа public, protected и private, хорошо знакомые большинству разработчиков:

class Order {

    private status: OrderStatus;

    constructor(public readonly id: string, isSpecialOrder: boolean) {
        [...]
    }
}

Теперь у класса Order два атрибута: приватный status и публичное поле id, доступное только для чтения. В TypeScript аргументы конструктора с ключевыми словами public, protected или private автоматически становятся атрибутами класса.

interface User {
    id?: string;
    name: string;
    t_registered: Date;
}

const user: User = { name: 'Bob', t_registered: new Date() };

Обратите внимание: поскольку в TypeScript используется вывод типов, объект User можно инстанцировать, даже если не предусмотрен класс User как таковой. Такой структуро-подобный подход часто выбирается при работе с чистыми сущностями данных и не требует никаких методов или внутреннего состояния.

Дженерики в TypeScript выражаются примерно таким же образом, что и в Java:

class Repository<T extends StoredEntity> {
    findOneById(id: string): T {
        [...]
    }
}

Мощная система типов


В основе мощной системы типов TypeScript лежит механизм вывода типов; также здесь поддерживается статическая типизация. Однако аннотации к статическим типам не являются обязательными, если возвращаемый тип или тип параметра можно вывести, исходя из контекста.

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

Перечисления, вывод типов и типы объединений


Рассмотрим обычную ситуацию, в которой у статуса заказа должно быть типобезопасное представление (в виде перечисления), но также требуется и строковое представление для сериализации в формате JSON. В Java для этого объявлялось бы enum вместе с конструктором и геттером для строковых значений.

В первом примере перечисления TypeScript позволяют напрямую добавить строковое представление. Таким образом, у нас в распоряжении оказывается типобезопасное представление в виде перечисления, автоматически сериализующее связанное с ним строковое представление.

enum Status {
    ORDER_RECEIVED = 'order_received',
    PAYMENT_RECEIVED = 'payment_received',
    DELIVERED = 'delivered',
}

interface Order {
    status: Status;
}

const order: Order = { status: Status.ORDER_RECEIVED };

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

Правда, оказывается, что при совместном использовании друг с другом вывода типов и типов-объединений эта задача может быть решена еще проще:

interface Order {
    status: 'order_received' | 'payment_received' | 'delivered';
}

const orderA: Order = { status: 'order_received' }; // скомпилируется
const orderB: Order = { status: 'new' }; // НЕ скомпилируется

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

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

Лямбды и функциональные аргументы


Поскольку TypeScript – это язык для функционального программирования, в его ядре есть поддержка анонимных функций, также именуемых лямбдами.

const evenNumbers = [ 1, 2, 3, 4, 5, 6 ].filter(i => i % 2 == 0);

В вышеприведенном примере .filter() принимает функцию типа (a: T) => boolean. Эта функция представлена анонимной лямбдой i => i % 2 == 0. В отличие от Java, где у функциональных параметров должен быть явно указан тип, функциональный интерфейс, тип лямбды также может быть представлен анонимно:

class OrderService {
    constructor(callback: (order: Order) => void) {
        [...]
    }
}

Асинхронное программирование


Поскольку TypeScript, при всех оговорках, является надмножеством JavaScript, асинхронное программирование — ключевая концепция этого языка. Да, здесь можно использовать лямбды и обратные вызовы, в TypeScript есть два важнейших механизма, помогающих избежать ада обратных вызовов: промисы и красивый паттерн async/await. Промис – это, в сущности, немедленно возвращаемое значение, обещающее вернуть конкретное значение позднее.

// асинхронная функция, возвращающая промис
function fetchUserProfiles(url: string): Promise<UserProfile[]> {
    [...]
}

// либо может использоваться вот так
function getActiveProfiles(): Promise<UserProfile[]> {
    return fetchUserProfiles(URL)
        .then(profiles => profiles.filter(profile => profile.active))
        .catch(error => handleError(error));
}

Поскольку инструкции .then() можно сцеплять в любом количестве, в некоторых случаях вышеприведенный паттерн может давать довольно запутанный код. Объявляя функцию async и используя await, дожидаясь, пока разрешится промис, можно записать этот же код в гораздо более синхронном стиле. Также в таком случае открывается возможность для использования хорошо известных операторов try/catch:

// использование async/await (выбрасывает ошибку, если fetchUserProfiles выбрасывает ошибку)
async function getActiveProfiles(): Promise<UserProfile[]> {
    const allProfiles = await fetchUserProfiles(URL);
    return allProfiles.filter(profile => profile.active);
}

// вариант с try/catch
async function getActiveProfilesSafe(): Promise<UserProfile[]> {
    try {
        const allProfiles = await fetchUserProfiles(URL);
        return allProfiles.filter(profile => profile.active);
    } catch (error) {
        handleError(error);
        return [];
    }
}

Обратите внимание: хотя, вышеприведенный код и выглядит синхронным, это лишь видимость (поскольку здесь возвращается еще один промис).

Оператор расширения и rest-оператор: упрощаем себе жизнь


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

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

Для начала воспользуемся оператором расширения массива… чтобы распаковать массив:

const a = [ 'a', 'b', 'c' ];
const b = [ 'd', 'e' ];

const result = [ ...a, ...b, 'f' ];
console.log(result);

// >> [ 'a', 'b', 'c', 'd', 'e', f' ]

Разумеется, это удобно, но настоящий TypeScript начинается, стоит вам осознать, что то же самое можно делать и с объектами:

interface UserProfile {
    userId: string;
    name: string;
    email: string;
    lastUpdated?: Date;
}

interface UserProfileUpdate {
    name?: string;
    email?: string;
}

const userProfile: UserProfile = { userId: 'abc', name: 'Bob', email: 'bob@example.com' };
const update: UserProfileUpdate = { email: 'bob@example.com' };

const updated: UserProfile = { ...userProfile, ...update, lastUpdated: new Date() };

console.log(updated);

// >> { userId: 'abc', name: 'Bob', email: 'bob@example.com', lastUpdated: 2019-12-19T16:09:45.174Z}

Рассмотрим, что здесь происходит. В принципе, объект updated создается при помощи конструктора с фигурными скобками. Внутри этого конструктора каждый параметр фактически создает новый объект, начиная работу слева.

Итак, используется расширенный объект userProfile; первым делом он копирует сам себя. На втором этапе расширенный объект update вливается в него и переприсваивается первому объекту; при этом, опять же, создается новый объект. На последнем шаге происходит слияние и переприсваивание поля lastUpdated, тогда создается новый объект и в результате – конечный объект.

Использование оператора расширения для создания копий иммутабельного объекта – очень безопасный и и быстрый способ обработки данных. Отметим: оператор расширения создает неглубокую копию объекта. Элементы с глубиной более единицы в таком случае копируются как ссылки.

У оператора расширения также есть эквивалент-деструктор, именуемый object rest:

const userProfile: UserProfile = { userId: 'abc', name: 'Bob', email: 'bob@example.com' };
const { userId, ...details } = userProfile;
console.log(userId);
console.log(details);

// >> 'abc'
// >> { name: 'Bob', email: 'bob@example.com' }

Здесь самое время просто откинуться на спинку кресла и вообразить весь тот код, который пришлось бы написать на Java для выполнения таких операций, как показанные выше.

Заключение. Немного о достоинствах и недостатках


Производительность


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

Тип number


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

Экосистема


Учитывая популярность Node.js, неудивительно, что сегодня для него существуют сотни тысяч пакетов. Но, поскольку Node моложе Java, многие пакеты пережили не так много версий, а качество кода в некоторых библиотеках явно оставляет желать лучшего.

Среди прочих стоит упомянуть несколько качественных библиотек, с которыми очень удобно работать: например, для веб-серверов, внедрения зависимостей и аннотаций контроллеров. Но, если сервис будет серьезно зависеть от многочисленных и хорошо поддерживаемых сторонних программ, то лучше воспользоваться Python, Java или Clojure.

Ускоренная разработка фич


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

Наконец, TypeScript хорошо сдобрен синтаксическим сахаром, поэтому разработка на нем идет приятно и быстро.
Теги:
Хабы:
+9
Комментарии 2
Комментарии Комментарии 2

Публикации

Информация

Сайт
piter.com
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия