TypeScript, найкращі практики при написанні коду

TypeScript

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

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

Найкраща практика 1: сувора перевірка типів

Почнемо з самих азів. Уявіть, що можете перехоплювати потенційні помилки ще до їх виникнення. Звучить дуже добре, щоб бути правдою? Але саме це дозволяє отримати сувора перевірка типів в TS. Ця найкраща практика пов’язана з перехопленням тонких багів, які тихо просочуються в код і викликають проблеми надалі.

Сувора перевірка типів дозволяє забезпечити, щоб усі змінні мали ті типи, які ви задумали. Наприклад, це означає, що у разі оголошення змінної з типом string TS переконається, щоб надане їй значення дійсно було рядком, а не числом. Це допомагає відловлювати помилки на ранніх стадіях та домагатися належної роботи коду.

Увімкнути режим суворої перевірки типів можна просто встановити параметр “strict”: true у файлі tsconfig.json (за замовчуванням має бути true). В результаті TS активує набір перевірок, що перехоплюють певні помилки, які інакше залишилися б непоміченими.

Ось приклад того, як перевірка типів може позбавити вас від поширеної помилки:

let userName: string = "John";
userName = 123; // TypeScript викине виключення, тому що "123" не є строкою.

Найкраща практика 2: виведення типів

TypeScript побудований навколо конкретного позначення типів, але це означає, що ви повинні все визначати явно.

Висновок типів – це можливість компілятора TS автоматично визначати тип змінної з урахуванням присвоєного їй значення. Це означає, що вам не потрібно явно вказувати тип змінної при кожному оголошенні. Натомість компілятор буде аналізувати присвоєне їй значення і виводити відповідний тип.

Наприклад, у наступному фрагменті коду TS автоматично виведе тип name як string:
<prelet name = “John”;

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

Але тут треба пам’ятати, що виведення типів – це не чарівна паличка, і іноді краще вказувати типи явно, особливо при роботі зі складними варіантами або у випадках, коли потрібно використовувати певний тип.

Найкраща практика 3: лінтери

Лінтери – це інструменти, які допомагають покращити код за рахунок його перевірки на відповідність набору правил та посібників.

Для TS існує кілька лінтерів на зразок TSLint і ESLint, які допомагають досягти узгодженості стилю коду та перехопити потенційні помилки. Ці лінтери можна налаштувати на виявлення таких помилок, як втрачені крапки з комою, змінні, що не використовуються, і не тільки.

Найкраща практика 4: інтерфейси

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

Інтерфейс в TS визначає договір форми об’єкта. Він показує властивості та методи, якими об’єкт даного типу повинен володіти, і може використовуватися як тип змінної. Це означає, що у разі присвоєння об’єкту змінної з типом interface TS перевірятиме, щоб цей об’єкт мав усі вказані в даному інтерфейсі властивості та методи.

Ось приклад визначення та використання інтерфейсу:

interface User {
 name: string;
 age: number;
}
let user: User = {name: "John", age: 25};

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

Найкраща практика 5: псевдоніми типів (type alias)

TypeScript дозволяє створювати кастомні типи, використовуючи звані псевдоніми типів. Основна відмінність між ними та інтерфейсами в тому, що при використанні псевдоніма ми створюємо нове ім’я для типу, а інтерфейс створює нове ім’я для форми об’єкта.

Наприклад, псевдонім типу можна використовувати, щоб створити власний тип для точки у двовимірному просторі:

type Point = { x: number, y: number };
let point: Point = { x: 0, y: 0 };

За допомогою псевдонімів типів також можна створювати складні типи типу об’єднаного типу або типу перетину.

type User = { name: string, age: number };
type Admin = { name: string, age: number, privileges: string[] };
type SuperUser = User & Admin;

Найкраща практика 6: кортежі

Кортежі дозволяють створювати масив фіксованого розміру з елементами різних типів, які розташовані в певному порядку.

Наприклад, за допомогою кортежу можна уявити ту саму точку у двовимірному просторі:

let point: [number, number] = [1, 2];

З їх допомогою можна також представляти колекцію елементів декількох типів:

let user: [string, number, boolean] = ["Bob", 25, true];

Однією з основних властивостей кортежів є те, що вони дають можливість виразити конкретний зв’язок між елементами колекції та типами.

Крім цього, за допомогою деструктуруючого присвоєння можна витягувати елементи кортежу та привласнювати їх змінним:

let point: [number, number] = [1, 2];
let [x, y] = point;
console.log(x, y);

Найкраща практика 7: тип any

Іноді у нас немає всієї інформації про тип змінної, але використовувати її в коді все ж таки потрібно. У подібних випадках можна задіяти тип any. Однак, як і у випадку з будь-яким потужним інструментом, any необхідно бути дуже обережним.

Один з кращих прийомів полягає в обмеженні застосування any до конкретних випадків, в яких тип дійсно невідомий. Це буває при роботі зі сторонніми бібліотеками або даними, що динамічно генеруються. Крім того, буде зайвим додавати затвердження типів або тайп гарди (type guard), гарантуючи правильне використання змінної. Також по можливості намагайтеся максимально звузити тип змінної.

Наприклад:

function logData(data: any) {
    console.log(data);
}

const user = { name: "John", age: 30 };
const numbers = [1, 2, 3];

logData(user); // { name: "John", age: 30 }
logData(numbers); // [1, 2, 3]

Ще один корисний прийом полягає в уникненні використання any для аргументів функцій і типів, що повертаються їй, оскільки він може знизити загальну безпеку типів коду. Натомість рекомендується використовувати більш конкретний тип або більш узагальнений на зразок unknown або object, який забезпечить будь-який рівень безпеки типів.

Найкраща практика 8: тип unknown

Unknown– це рестриктивний тип, введений у TypeScript 3.0. Він більш обмежений у порівнянні з any і може позбавити вас від низки непередбачених помилок.

На відміну від any використання unknown компілятор не дасть виконати будь-яку операцію для значення без попередньої перевірки його типу. Це дозволить перехопити помилки типів під час компіляції, а чи не серед виконання.

Наприклад, тип unknown можна використовувати для створення більш типобезпечної функції:

function printValue(value: unknown) {
 if (typeof value === "string") {
 console.log(value);
 } else {
 console.log("Not a string");
 }
}

З його допомогою також можна створювати типобезпечніші змінні:

let value: unknown = "hello";
let str: string = value; // Помилка: тип 'unknown' неможна присвоювати типу 'string'.

Найкраща практика 9: тип Object

Тип Object є вбудованою можливістю TypeScript, що дозволяє посилатися базовий тип об’єкта. З його допомогою можна підвищити типобезпечність коду, забезпечивши наявність у всіх об’єктів певних властивостей чи методів.

Наприклад, цей тип можна використовувати для створення більш типобезпечної функції, що отримує як аргумент об’єкт:

function printObject(obj: Object) {
 console.log(obj);
}

Тип Object також можна використовувати для створення більш типобезпечних змінних:

let obj: Object = { name: "John", age: 30 };
let str: string = obj.name; // валідно
let num: number = obj.age; // валідно

За допомогою нього ви можете забезпечити, щоб всі об’єкти мали певні властивості або методи, підвищивши тим самим типобезпеку коду.

Найкраща практика 10: тип never

TypeScript never є особливим типом, що представляє значення, яке ніколи не з’явиться. З його допомогою вказують, що функція замість стандартного повернення викидатиме помилку. Це чудовий спосіб, що дозволяє показати іншим розробникам (і компілятору), що функцію не можна використовувати певним чином.

Розгляньте, наприклад, наступну функцію, що викидає помилку при знаменнику рівному 0:

function divide(numerator: number, denominator: number): number {
 if (denominator === 0) {
 throw new Error("Cannot divide by zero");
 }
 return numerator / denominator;
}

Тут функція divide оголошена як число, що повертає, але якщо знаменник дорівнює нулю, вона викидає помилку. Щоб позначити неможливість у такому разі виконати повернення, можна використовувати як тип, що повертається never:

function divide(numerator: number, denominator: number): number | never {
 if (denominator === 0) {
 throw new Error("Cannot divide by zero");
 }
 return numerator / denominator;
}

Найкраща практика 11: оператор keyof

Оператор keyof є ефективною можливістю TypeScript, що дозволяє створювати тип, що представляє ключі об’єкта. З його допомогою можна проясняти, які властивості для об’єкта є допустимими.

Наприклад, keyof можна використовувати створення більш читаного і обслуговуваного типу об’єкта:

interface User {
 name: string;
 age: number;
}
type UserKeys = keyof User; // "name" | "age"

З його допомогою також можна створювати більш типобезпечні функції, що отримують як аргументи об’єкт і ключ:

function getValue<T, K extends keyof T>(obj: T, key: K) {
 return obj[key];
}
let user: User = { name: "John", age: 30 };
console.log(getValue(user, "name")); // "John"
console.log(getValue(user, "gender")); // Помилка: аргумент з типом '"gender"' неможна присвоїти параметру з типом '"name" | "age"'.

Найкраща практика 12: Enums

Enums– Це перерахування, що дозволяють визначати в TS набір іменованих констант. З їх допомогою можна писати більш читаний та обслуговуваний код, даючи набору зв’язаних значень ім’я, що відображає загальний зміст.

Наприклад, enum можна визначити набір можливих значень статусу замовлення:

enum OrderStatus {
 Pending,
 Processing,
 Shipped,
 Delivered,
 Cancelled
}
let orderStatus: OrderStatus = OrderStatus.Pending;

Перерахування також можуть мати кастомний набір чисельних значень чи рядків.

enum OrderStatus {
 Pending = 1,
 Processing = 2,
 Shipped = 3,
 Delivered = 4,
 Cancelled = 5
}
let orderStatus: OrderStatus = OrderStatus.Pending;

Угода про іменування вимагає називати перерахування з великої літери і вказувати їх завжди в однині.

Найкраща практика 13: простор імен

Простір імен дозволяють організовувати код і уникати колізій в іменуванні його елементів. За допомогою їх можна створити для коду контейнер, в якому визначити змінні, класи, функції та інтерфейси.

Наприклад, простір імен можна використовувати для групування всього коду, пов’язаного з певним функціоналом:

namespace OrderModule {
 export class Order { /* … */ }
 export function cancelOrder(order: Order) { /* … */ }
 export function processOrder(order: Order) { /* … */ }
}
let order = new OrderModule.Order();
OrderModule.cancelOrder(order);

Простір імен також дозволяють запобігти колізії в іменуванні за рахунок присвоєння фрагменту коду унікального імені:

namespace MyCompany.MyModule {
 export class MyClass { /* … */ }
}
let myClass = new MyCompany.MyModule.MyClass();

Важливо, що простори імен аналогічні модулям, але використовуються для організації коду та уникнення колізій, у той час як модулі служать для завантаження та виконання коду.

Найкраща практика 14: допоміжні типи (utility types)

Допоміжні типи – це вбудована можливість TS, яка надає набір готових типів, що дозволяє писати більш типобезпечний код. З їхньою допомогою можна зручніше виконувати стандартні операції над типами.

Наприклад, допоміжний тип Pick можна використовуватиме вилучення підмножини властивостей із типу об’єкта:

type User = { name: string, age: number, email: string };
type UserInfo = Pick<User, "name" | "email">;
Exclude дозволяє видалити з типу об'єкта властивості:
type User = { name: string, age: number, email: string };
type UserWithoutAge = Exclude<User, "age">;

Partial дає можливість зробити всі властивості типу необов’язковими:

type User = { name: string, age: number, email: string };
type PartialUser = Partial<User>;

Найкраща практика 15: Readonly та ReadonlyArray

При роботі з даними в TypeScript іноді потрібно забезпечити незмінність певних значень, і тут на допомогу приходять ключові слова Readonly та ReadonlyArray.

За допомогою Readonly ми переводимо властивості об’єкта в стан «тільки для читання», виключаючи можливість зміни після створення. Такий прийом придатний, наприклад, при роботі з конфігурацією або постійними значеннями.

interface Point {
 x: number;
 y: number;
}
let point: Readonly<Point> = {x: 0, y: 0};
point.x = 1; // TypeScript видасть помилку, тому що "point.x" є read-only

ReadonlyArray аналогічно Readonly, але використовується для масивів.

let numbers: ReadonlyArray<number> = [1, 2, 3];
numbers.push(4); // TypeScript видасть помилку, тому що "numbers" є read-only

Найкраща практика 16: type guards

Працюючи зі складними типами буває складно відстежувати можливі варіації змінної. У цьому випадку ми маємо тайп-гарди, які дозволяють звузити тип змінної на основі певних умов.

Ось приклад використання тайп-гарду для перевірки, чи є змінна числом:

function isNumber(x: any): x is number {
 return typeof x === "number";
}
let value = 3;
if (isNumber(value)) {
 value.toFixed(2); // Завдяки тайп-гарду TypeScript знає, що "value" є числом.
}

Тайп-гарди також можна використовувати з операторами in і typeofinstanceof

Найкраща практика 17: узагальнені типи (generics)

Дженерики дозволяють писати універсальніший код, здатний працювати з будь-яким типом. З їхньою допомогою можна написати одну функцію, клас чи інтерфейс, які працюють із кількома типами.

Наприклад, узагальнену функцію можна використовуватиме створення масиву будь-якого типу:

function createArray<T>(length: number, value: T): Array<T> {
 let result = [];
 for (let i = 0; i < length; i++) {
 result[i] = value;
 }
 return result;
}
let names = createArray<string>(3, "Bob");
let numbers = createArray<number>(3, 0);

За допомогою дженериків також можна створити клас, здатний працювати з будь-яким типом даних:

class GenericNumber<T> {
 zeroValue: T;
 add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

Найкраща практика 18: ключове слово infer

Ключове слово infer є просунутою можливістю TS, дозволяє витягти тип змінної в окремий тип.

Наприклад, infer можна створити більш точний тип для функції, що повертає масив конкретного типу:

type ArrayType<T> = T extends (infer U)[] ? U : never;
type MyArray = ArrayType<string[]>; // MyArray має тип string

За допомогою нього також можна створювати точніший тип для функції, що повертає об’єкт з конкретною властивістю:

type ObjectType<T> = T extends { [key: string]: infer U } ? U : never;
type MyObject = ObjectType<{ name: string, age: number }>; // MyObject має тип {name:string, age: number}

Найкраща практика 19: умовні типи

З допомогою умовних типів можна створювати нові типи з урахуванням умов інших типів, висловлюючи цим складні відносини з-поміж них.

Наприклад, умовний тип можна використовувати для отримання типу функції, що повертається:

type ReturnType<T> = T extends (…args: any[]) => infer R ? R : any;
type R1 = ReturnType<() => string>; // string
type R2 = ReturnType<() => void>; // void

Крім цього, з їх допомогою можна отримувати властивості типу об’єкта, що відповідають конкретній умові:

type PickProperties<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];
type P1 = PickProperties<{ a: number, b: string, c: boolean }, string | number>; // "a" | "b"

Найкраща практика 20: відображені типи

Відображені типи є способом створення нових типів на основі існуючих шляхом застосування ряду операцій до їх властивостей.

Наприклад, за допомогою відображеного типу можна отримати новий тип, що представляє варіацію існуючого, але вже для читання:

type Readonly<T> = { readonly [P in keyof T]: T[P] };
let obj: { a: number, b: string } = { a: 1, b: "hello" };
let readonlyObj: Readonly<typeof obj> = { a: 1, b: "hello" };

Вони також дозволяють створювати новий тип, що представляє опціональну версію:

type Optional<T> = { [P in keyof T]?: T[P] };
let obj: { a: number, b: string } = { a: 1, b: "hello" };
let optionalObj: Optional<typeof obj> = { a: 1 };

Відображені типи можна використовувати по-різному: для створення нових, а також додавання, видалення або зміни властивостей наявних.

Найкраща практика 21: декоратори

Декоратори представляють спосіб внесення додаткової функціональності до класу, методу або властивості за допомогою простого синтаксису. Вони дозволяють розширити поведінку класу без його реалізації.

Наприклад, за допомогою декоратора можна додати метод логування:

function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
 let originalMethod = descriptor.value;
 descriptor.value = function(…args: any[]) {
 console.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`);
 let result = originalMethod.apply(this, args);
 console.log(`Called ${propertyKey}, result: ${result}`);
 return result;
 }
}
class Calculator {
 @logMethod
 add(x: number, y: number): number {
 return x + y;
 }
}

Декоратори також дозволяють додавати до класу, методу або властивості метадані, які можуть використовуватися в середовищі виконання.

function setApiPath(path: string) {
 return function (target: any) {
 target.prototype.apiPath = path;
 }
}
@setApiPath("/users")
class UserService {
 // …
}
console.log(new UserService().apiPath); // "/users"

Висновок

Сподіваюся, що ця стаття виявилася для вас корисною і розширила ваше розуміння того, як можна писати чистіший та ефективніший код.

Пам’ятайте, все це лише найкращі практики та керівництва, а не жорсткі правила, тому насамперед тут потрібно спиратися на власне розуміння та здоровий глузд. Також важливо враховувати, що в міру розвитку цієї мови та появи в ній нових можливостей кращі практики можуть змінюватися, а отже, потрібно бути готовим до освоєння нової.

Переклад статті “Mastering TypeScript: 21 Best Practices for Improved Code Quality