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

TypeScript для бекенд-розробки
13 хв. читання
01 листопада 2020

Java все ще являється мовою яку найчастіше вибирають для бекенд розробки. На це є чимало причин: швидкість, безпека (якщо, звичайно, закрити очі на null-покажчики), плюс велика, добре протестована екосистема. Але в еру мікросервісов та гнучкої розробки стали важливіші й інші фактори. В деяких системах буває не обов'язково тримати пікову продуктивність і мати екосистему  із  стабільними залежностями, якщо мова йде про простенький сервіс, що виконує CRUD-операції і перетворення даних. Більше того, багато систем доводиться оперативно будувати і перебудовувати, щоб йти в ногу зі швидкою та інтерактивною розробкою фіч.

Простий сервіс на Java досить легко розробити та розгорнути, завдяки  магії Spring Boot. Але, оскільки замкнуті класи доводиться тестувати, а дані -- перетворювати, в коді з'являється маса білдерів, перетворювачів, конструкторів перерахувань і серіалізаторів, і всім цим вимощений жахливий шлях до пекла стереотипного коду Java. Саме тому часто затримується розробка нових фіч. І, так, генерація коду працює, але це не дуже гнучкий прийом.

TypeScript поки не встиг добре зарекомендувати себе серед бекенд-розробників. Ймовірно, тому що відомий як набір декларативних файлів, що дозволяють додати в JavaScript деяку типізацію. Але, все ж, є маса логіки, на представлення якої пішли б десятки рядків на Java, і яку можна представити всього в декількох рядках TypeScript.

Маса фіч, про які говорять як про характерні риси TypeScript, насправді відносяться до JavaScript. Але TypeScript також можна розглядати як самостійну мова, яка володіє певним синтаксичною та концептуальною схожістю з JavaScript. Тому давайте ненадовго відвернемося від JavaScript і розглянемо TypeScript сам по собі: це красива мову з виключно потужною, але при цьому гнучкою системою типів, з купою синтаксичного цукру і, нарешті, з нуль-безпекою!

Ми розмістили на Github репозиторій зі спеціально розробленим веб-застосунком на Node/TypeScript, а також з деякими додатковими поясненнями. Там є також гілка зі складнішим кодом з прикладом Onion Architecture  і нетривіальнішими концепціями з області типізації.

Знайомство з 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 {
    findOneById(id: string): T {
        [...]
    }
}

Потужна система типів

В основі потужної системи типів TypeScript лежить механізм виведення типів; також тут підтримується статична типізація. Однак анотації статичного типу не обов'язкові, якщо значення що повертається функціїю або тип параметра можна отримати з контексту.

TypeScript також дозволяє використовувати типи об'єднань, часткові типи і перетини типів, завдяки чому мова набуває значну гнучкість, в той же час уникаючи зайвої складності. Мова також дозволяє використовувати фактичні значення як тип, що є неймовірно зручним у багатьох ситуаціях.

Перерахування, виведення типів і типи об'єднань

Розглянемо загальну ситуацію, коли статус замовлення повинен мати безпечне представлення типу таке як enum, але також потрібно і строкове представлення для серіалізації JSON. В Java для цього треба було б оголосити enum разом із конструктором та геттером для рядкових значень.

У першому прикладі перерахування TypeScript дозволяють безпосередньо додати строкове представлення. Таким чином, ми отримуємо в розпорядження безпечне представлення enum, яке автоматично серіалізуются в пов'язане з ним представлення у вигляді рядка.

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

interface Order {
    status: Status;
}

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

Зверніть увагу на останній рядок коду, де вивід типів дозволяє нам інстанціювати об'єкт, що підходить під інтерфейс `Order`. Оскільки наш `Order` не потребує внутрішнього стану чи логіки, класи та конструктори не потрібні.

Однак, використовуючи комбінацію виводу типу та типів об'єднання, ця задача може вирішитися ще простіше:

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

const orderA: Order = { status: 'order_received' }; // will compile
const orderB: Order = { status: 'new' }; // will NOT compile

Компілятор 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. Проміс - це, по суті, значення що повертається негайно, яке обіцяє повернути конкретне значення пізніше.

// an asynchronous function returning a promise
function fetchUserProfiles(url: string): Promise<UserProfile[]> {
    [...]
}

// could either be used like this
function getActiveProfiles(): Promise<UserProfile[]> {
    return fetchUserProfiles(URL)
        .then(profiles => profiles.filter(profile => profile.active))
        .catch(error => handleError(error));
}

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

// using async/await (throws an error if fetchUserProfiles throws an error)
async function getActiveProfiles(): Promise<UserProfile[]> {
    const allProfiles = await fetchUserProfiles(URL);
    return allProfiles.filter(profile => profile.active);
}

// or with 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 добре присмачений синтаксичним цукром, тому розробка на ньому йде приємно і швидко.

Джерело: https://www.innoq.com/en/blog/typescript-for-backend-development/

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація