Українська

Розкрийте потенціал незмінних структур даних у TypeScript за допомогою типів readonly. Дізнайтеся, як створювати більш передбачувані, надійні та простіші в обслуговуванні застосунки.

Типи Readonly в TypeScript: освоєння незмінних структур даних

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

Що таке імутабельність і чому це важливо?

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

Типи Readonly в TypeScript: ваш арсенал імутабельності

TypeScript надає кілька способів забезпечити імутабельність за допомогою ключового слова readonly. Розглянемо різні методи та способи їх застосування на практиці.

1. Властивості Readonly в інтерфейсах та типах

Найпростіший спосіб оголосити властивість як readonly — це використати ключове слово readonly безпосередньо в визначенні інтерфейсу або типу.


interface Person {
  readonly id: string;
  name: string;
  age: number;
}

const person: Person = {
  id: "unique-id-123",
  name: "Alice",
  age: 30,
};

// person.id = "new-id"; // Помилка: Неможливо присвоїти значення 'id', оскільки це властивість лише для читання.
person.name = "Bob"; // Це дозволено

У цьому прикладі властивість id оголошена як readonly. TypeScript запобігатиме будь-яким спробам змінити її після створення об'єкта. Властивості name та age, не маючи модифікатора readonly, можуть бути вільно змінені.

2. Допоміжний тип Readonly

TypeScript пропонує потужний допоміжний тип під назвою Readonly<T>. Цей узагальнений тип приймає існуючий тип T і перетворює його, роблячи всі його властивості readonly.


interface Point {
  x: number;
  y: number;
}

const point: Readonly<Point> = {
  x: 10,
  y: 20,
};

// point.x = 30; // Помилка: Неможливо присвоїти значення 'x', оскільки це властивість лише для читання.

Тип Readonly<Point> створює новий тип, у якому і x, і y є readonly. Це зручний спосіб швидко зробити існуючий тип незмінним.

3. Масиви лише для читання (ReadonlyArray<T>) та readonly T[]

Масиви в JavaScript за своєю природою є змінними. TypeScript надає спосіб створювати масиви лише для читання за допомогою типу ReadonlyArray<T> або скороченого запису readonly T[]. Це запобігає зміні вмісту масиву.


const numbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];
// numbers.push(6); // Помилка: Властивість 'push' не існує в типі 'readonly number[]'.
// numbers[0] = 10; // Помилка: Сигнатура індексу в типі 'readonly number[]' дозволяє лише читання.

const moreNumbers: readonly number[] = [6, 7, 8, 9, 10]; // Еквівалентно ReadonlyArray
// moreNumbers.push(11); // Помилка: Властивість 'push' не існує в типі 'readonly number[]'.

Спроба використати методи, що змінюють масив, такі як push, pop, splice, або пряме присвоєння за індексом, призведе до помилки TypeScript.

4. const проти readonly: розуміння різниці

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


const immutableNumber = 42;
// immutableNumber = 43; // Помилка: Неможливо переприсвоїти значення константній змінній 'immutableNumber'.

const mutableObject = { value: 10 };
mutableObject.value = 20; // Це дозволено, оскільки *об'єкт* не є константою, а лише змінна.

const readonlyObject: Readonly<{ value: number }> = { value: 30 };
// readonlyObject.value = 40; // Помилка: Неможливо присвоїти значення 'value', оскільки це властивість лише для читання.

const constReadonlyObject: Readonly<{ value: number }> = { value: 50 };
// constReadonlyObject = { value: 60 }; // Помилка: Неможливо переприсвоїти значення константній змінній 'constReadonlyObject'.
// constReadonlyObject.value = 60; // Помилка: Неможливо присвоїти значення 'value', оскільки це властивість лише для читання.

Як показано вище, const гарантує, що змінна завжди вказує на один і той же об'єкт у пам'яті, тоді як readonly гарантує, що внутрішній стан об'єкта залишається незмінним.

Практичні приклади: застосування типів Readonly у реальних сценаріях

Розглянемо кілька практичних прикладів того, як типи readonly можна використовувати для підвищення якості коду та його обслуговування в різних сценаріях.

1. Керування конфігураційними даними

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


interface AppConfig {
  readonly apiUrl: string;
  readonly timeout: number;
  readonly features: readonly string[];
}

const config: AppConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  features: ["featureA", "featureB"],
};

function fetchData(url: string, config: Readonly<AppConfig>) {
    // ... безпечно використовуйте config.timeout та config.apiUrl, знаючи, що вони не зміняться
}

fetchData("/data", config);

2. Реалізація керування станом на зразок Redux

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


interface State {
  readonly count: number;
  readonly items: readonly string[];
}

const initialState: State = {
  count: 0,
  items: [],
};

function reducer(state: Readonly<State>, action: { type: string; payload?: any }): State {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 }; // Повернути новий об'єкт стану
    case "ADD_ITEM":
      return { ...state, items: [...state.items, action.payload] }; // Повернути новий об'єкт стану з оновленими елементами
    default:
      return state;
  }
}

3. Робота з відповідями API

При отриманні даних з API часто бажано розглядати дані відповіді як незмінні, особливо якщо ви використовуєте їх для рендерингу компонентів UI. Типи readonly можуть допомогти запобігти випадковим змінам даних API.


interface ApiResponse {
  readonly userId: number;
  readonly id: number;
  readonly title: string;
  readonly completed: boolean;
}

async function fetchTodo(id: number): Promise<Readonly<ApiResponse>> {
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
  const data: ApiResponse = await response.json();
  return data;
}

fetchTodo(1).then(todo => {
  console.log(todo.title);
  // todo.completed = true; // Помилка: Неможливо присвоїти значення 'completed', оскільки це властивість лише для читання.
});

4. Моделювання географічних даних (міжнародний приклад)

Розглянемо представлення географічних координат. Після встановлення координати вона в ідеалі повинна залишатися незмінною. Це забезпечує цілісність даних, особливо при роботі з чутливими застосунками, такими як системи картографування або навігації, що працюють у різних географічних регіонах (наприклад, GPS-координати для служби доставки, що охоплює Північну Америку, Європу та Азію).


interface GeoCoordinates {
 readonly latitude: number;
 readonly longitude: number;
}

const tokyoCoordinates: GeoCoordinates = {
 latitude: 35.6895,
 longitude: 139.6917
};

const newYorkCoordinates: GeoCoordinates = {
 latitude: 40.7128,
 longitude: -74.0060
};


function calculateDistance(coord1: Readonly<GeoCoordinates>, coord2: Readonly<GeoCoordinates>): number {
 // Уявіть складне обчислення з використанням широти та довготи
 // Повертаємо умовне значення для простоти
 return 1000; 
}

const distance = calculateDistance(tokyoCoordinates, newYorkCoordinates);
console.log("Відстань між Токіо та Нью-Йорком (умовне значення):", distance);

// tokyoCoordinates.latitude = 36.0; // Помилка: Неможливо присвоїти значення 'latitude', оскільки це властивість лише для читання.

Глибоко незмінні типи: робота з вкладеними об'єктами

Допоміжний тип Readonly<T> робить readonly лише прямі властивості об'єкта. Якщо об'єкт містить вкладені об'єкти або масиви, ці вкладені структури залишаються змінними. Щоб досягти справжньої глибокої імутабельності, вам потрібно рекурсивно застосувати Readonly<T> до всіх вкладених властивостей.

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


type DeepReadonly<T> = T extends (infer R)[]
  ? DeepReadonlyArray<R>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
  };
  employees: string[];
}

const company: DeepReadonly<Company> = {
  name: "Example Corp",
  address: {
    street: "123 Main St",
    city: "Anytown",
    country: "USA",
  },
  employees: ["Alice", "Bob"],
};

// company.name = "New Corp"; // Помилка
// company.address.city = "New City"; // Помилка
// company.employees.push("Charlie"); // Помилка

Цей тип DeepReadonly<T> рекурсивно застосовує Readonly<T> до всіх вкладених властивостей, гарантуючи, що вся структура об'єкта є незмінною.

Що варто враховувати та які є компроміси

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

Бібліотеки для незмінних структур даних

Кілька бібліотек можуть спростити роботу з незмінними структурами даних у TypeScript:

Найкращі практики використання типів Readonly

Щоб ефективно використовувати типи readonly у ваших проєктах TypeScript, дотримуйтесь цих найкращих практик:

Висновок: впровадження імутабельності за допомогою типів Readonly у TypeScript

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