Українська

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

Літеральні типи в TypeScript: освоєння точних обмежень значень

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

Що таке літеральні типи?

На відміну від традиційних типів, таких як string, number або boolean, літеральні типи не представляють широку категорію значень. Натомість вони представляють конкретні, фіксовані значення. TypeScript підтримує три види літеральних типів:

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

Рядкові літеральні типи

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

Базовий синтаксис

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


type AllowedValues = "value1" | "value2" | "value3";

Це визначає тип з назвою AllowedValues, який може містити лише рядки "value1", "value2" або "value3".

Практичні приклади

1. Визначення палітри кольорів:

Уявіть, що ви створюєте UI-бібліотеку і хочете переконатися, що користувачі можуть вказувати кольори лише з попередньо визначеної палітри:


type Color = "red" | "green" | "blue" | "yellow";

function paintElement(element: HTMLElement, color: Color) {
  element.style.backgroundColor = color;
}

paintElement(document.getElementById("myElement")!, "red"); // Правильно
paintElement(document.getElementById("myElement")!, "purple"); // Помилка: Аргумент типу '"purple"' не може бути присвоєний параметру типу 'Color'.

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

2. Визначення кінцевих точок API:

При роботі з API часто потрібно вказувати дозволені кінцеві точки. Рядкові літеральні типи можуть допомогти це забезпечити:


type APIEndpoint = "/users" | "/posts" | "/comments";

function fetchData(endpoint: APIEndpoint) {
  // ... реалізація для отримання даних із зазначеної кінцевої точки
  console.log(`Отримання даних з ${endpoint}`);
}

fetchData("/users"); // Правильно
fetchData("/products"); // Помилка: Аргумент типу '"/products"' не може бути присвоєний параметру типу 'APIEndpoint'.

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

3. Обробка різних мов (інтернаціоналізація - i18n):

У глобальних додатках може знадобитися обробка різних мов. Ви можете використовувати рядкові літеральні типи, щоб переконатися, що ваш додаток підтримує лише зазначені мови:


type Language = "en" | "es" | "fr" | "de" | "zh";

function translate(text: string, language: Language): string {
  // ... реалізація для перекладу тексту на вказану мову
  console.log(`Переклад '${text}' на ${language}`);
  return "Перекладений текст"; // Заглушка
}

translate("Hello", "en"); // Правильно
translate("Hello", "ja"); // Помилка: Аргумент типу '"ja"' не може бути присвоєний параметру типу 'Language'.

Цей приклад демонструє, як забезпечити використання у вашому додатку лише підтримуваних мов.

Числові літеральні типи

Числові літеральні типи дозволяють вказати, що змінна або властивість може містити лише конкретне числове значення.

Базовий синтаксис

Синтаксис для визначення числового літерального типу схожий на синтаксис для рядкових літеральних типів:


type StatusCode = 200 | 404 | 500;

Це визначає тип з назвою StatusCode, який може містити лише числа 200, 404 або 500.

Практичні приклади

1. Визначення кодів стану HTTP:

Ви можете використовувати числові літеральні типи для представлення кодів стану HTTP, гарантуючи, що у вашому додатку використовуються лише дійсні коди:


type HTTPStatus = 200 | 400 | 401 | 403 | 404 | 500;

function handleResponse(status: HTTPStatus) {
  switch (status) {
    case 200:
      console.log("Успіх!");
      break;
    case 400:
      console.log("Неправильний запит");
      break;
    // ... інші випадки
    default:
      console.log("Невідомий статус");
  }
}

handleResponse(200); // Правильно
handleResponse(600); // Помилка: Аргумент типу '600' не може бути присвоєний параметру типу 'HTTPStatus'.

Цей приклад забезпечує використання дійсних кодів стану HTTP, запобігаючи помилкам, спричиненим використанням неправильних або нестандартних кодів.

2. Представлення фіксованих опцій:

Ви можете використовувати числові літеральні типи для представлення фіксованих опцій в об'єкті конфігурації:


type RetryAttempts = 1 | 3 | 5;

interface Config {
  retryAttempts: RetryAttempts;
}

const config1: Config = { retryAttempts: 3 }; // Правильно
const config2: Config = { retryAttempts: 7 }; // Помилка: Тип '{ retryAttempts: 7; }' не може бути присвоєний типу 'Config'.

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

Булеві літеральні типи

Булеві літеральні типи представляють конкретні значення true або false. Хоча вони можуть здаватися менш універсальними, ніж рядкові або числові літеральні типи, вони можуть бути корисними в певних сценаріях.

Базовий синтаксис

Синтаксис для визначення булевого літерального типу:


type IsEnabled = true | false;

Однак пряме використання true | false є надлишковим, оскільки це еквівалентно типу boolean. Булеві літеральні типи корисніші в поєднанні з іншими типами або в умовних типах.

Практичні приклади

1. Умовна логіка з конфігурацією:

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


interface FeatureFlags {
  darkMode: boolean;
  newUserFlow: boolean;
}

function initializeApp(flags: FeatureFlags) {
  if (flags.darkMode) {
    // Увімкнути темний режим
    console.log("Увімкнення темного режиму...");
  } else {
    // Використовувати світлий режим
    console.log("Використання світлого режиму...");
  }

  if (flags.newUserFlow) {
    // Увімкнути новий потік користувача
    console.log("Увімкнення нового потоку користувача...");
  } else {
    // Використовувати старий потік користувача
    console.log("Використання старого потоку користувача...");
  }
}

initializeApp({ darkMode: true, newUserFlow: false });

Хоча в цьому прикладі використовується стандартний тип boolean, ви можете поєднати його з умовними типами (пояснено далі) для створення складнішої поведінки.

2. Дискриміновані об'єднання:

Булеві літеральні типи можна використовувати як дискримінатори в об'єднаних типах. Розглянемо наступний приклад:


interface SuccessResult {
  success: true;
  data: any;
}

interface ErrorResult {
  success: false;
  error: string;
}

type Result = SuccessResult | ErrorResult;

function processResult(result: Result) {
  if (result.success) {
    console.log("Успіх:", result.data);
  } else {
    console.error("Помилка:", result.error);
  }
}

processResult({ success: true, data: { name: "John" } });
processResult({ success: false, error: "Не вдалося отримати дані" });

Тут властивість success, яка є булевим літеральним типом, діє як дискримінатор, дозволяючи TypeScript звузити тип result всередині оператора if.

Поєднання літеральних типів з об'єднаними типами

Літеральні типи є найпотужнішими в поєднанні з об'єднаними типами (використовуючи оператор |). Це дозволяє визначити тип, який може містити одне з кількох конкретних значень.

Практичні приклади

1. Визначення типу статусу:


type Status = "pending" | "in progress" | "completed" | "failed";

interface Task {
  id: number;
  description: string;
  status: Status;
}

const task1: Task = { id: 1, description: "Implement login", status: "in progress" }; // Правильно
const task2: Task = { id: 2, description: "Implement logout", status: "done" };       // Помилка: Тип '{ id: number; description: string; status: string; }' не може бути присвоєний типу 'Task'.

Цей приклад демонструє, як забезпечити дотримання певного набору дозволених значень статусу для об'єкта Task.

2. Визначення типу пристрою:

У мобільному додатку вам може знадобитися обробляти різні типи пристроїв. Для їх представлення можна використовувати об'єднання рядкових літеральних типів:


type DeviceType = "mobile" | "tablet" | "desktop";

function logDeviceType(device: DeviceType) {
  console.log(`Тип пристрою: ${device}`);
}

logDeviceType("mobile"); // Правильно
logDeviceType("smartwatch"); // Помилка: Аргумент типу '"smartwatch"' не може бути присвоєний параметру типу 'DeviceType'.

Цей приклад гарантує, що функція logDeviceType викликається лише з дійсними типами пристроїв.

Літеральні типи з псевдонімами типів

Псевдоніми типів (з використанням ключового слова type) надають спосіб дати ім'я літеральному типу, роблячи ваш код більш читабельним та легким в обслуговуванні.

Практичні приклади

1. Визначення типу коду валюти:


type CurrencyCode = "USD" | "EUR" | "GBP" | "JPY";

function formatCurrency(amount: number, currency: CurrencyCode): string {
  // ... реалізація для форматування суми відповідно до коду валюти
  console.log(`Форматування ${amount} в ${currency}`);
  return "Відформатована сума"; // Заглушка
}

formatCurrency(100, "USD"); // Правильно
formatCurrency(200, "CAD"); // Помилка: Аргумент типу '"CAD"' не може бути присвоєний параметру типу 'CurrencyCode'.

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

2. Визначення типу дня тижня:


type DayOfWeek = "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday";

function isWeekend(day: DayOfWeek): boolean {
  return day === "Saturday" || day === "Sunday";
}

console.log(isWeekend("Monday"));   // false
console.log(isWeekend("Saturday")); // true
console.log(isWeekend("Funday"));   // Помилка: Аргумент типу '"Funday"' не може бути присвоєний параметру типу 'DayOfWeek'.

Виведення літеральних типів

TypeScript часто може автоматично виводити літеральні типи на основі значень, які ви присвоюєте змінним. Це особливо корисно при роботі зі змінними const.

Практичні приклади

1. Виведення рядкових літеральних типів:


const apiKey = "your-api-key"; // TypeScript виводить тип apiKey як "your-api-key"

function validateApiKey(key: "your-api-key") {
  return key === "your-api-key";
}

console.log(validateApiKey(apiKey)); // true

const anotherKey = "invalid-key";
console.log(validateApiKey(anotherKey)); // Помилка: Аргумент типу 'string' не може бути присвоєний параметру типу '"your-api-key"'.

У цьому прикладі TypeScript виводить тип apiKey як рядковий літеральний тип "your-api-key". Однак, якщо ви присвоюєте неконстантне значення змінній, TypeScript зазвичай виведе ширший тип string.

2. Виведення числових літеральних типів:


const port = 8080; // TypeScript виводить тип port як 8080

function startServer(portNumber: 8080) {
  console.log(`Запуск сервера на порту ${portNumber}`);
}

startServer(port); // Правильно

const anotherPort = 3000;
startServer(anotherPort); // Помилка: Аргумент типу 'number' не може бути присвоєний параметру типу '8080'.

Використання літеральних типів з умовними типами

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

Базовий синтаксис

Синтаксис умовного типу:


TypeA extends TypeB ? TypeC : TypeD

Це означає: якщо TypeA можна присвоїти TypeB, то результуючим типом буде TypeC; в іншому випадку результуючим типом буде TypeD.

Практичні приклади

1. Зіставлення статусу з повідомленням:


type Status = "pending" | "in progress" | "completed" | "failed";

type StatusMessage = T extends "pending"
  ? "Очікування дії"
  : T extends "in progress"
  ? "Зараз обробляється"
  : T extends "completed"
  ? "Завдання успішно завершено"
  : "Сталася помилка";

function getStatusMessage(status: T): StatusMessage {
  switch (status) {
    case "pending":
      return "Очікування дії" as StatusMessage;
    case "in progress":
      return "Зараз обробляється" as StatusMessage;
    case "completed":
      return "Завдання успішно завершено" as StatusMessage;
    case "failed":
      return "Сталася помилка" as StatusMessage;
    default:
      throw new Error("Недійсний статус");
  }
}

console.log(getStatusMessage("pending"));    // Очікування дії
console.log(getStatusMessage("in progress")); // Зараз обробляється
console.log(getStatusMessage("completed"));   // Завдання успішно завершено
console.log(getStatusMessage("failed"));      // Сталася помилка

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

2. Створення типобезпечного обробника подій:


type EventType = "click" | "mouseover" | "keydown";

type EventData = T extends "click"
  ? { x: number; y: number; } // Дані події click
  : T extends "mouseover"
  ? { target: HTMLElement; }   // Дані події mouseover
  : { key: string; }             // Дані події keydown

function handleEvent(type: T, data: EventData) {
  console.log(`Обробка події типу ${type} з даними:`, data);
}

handleEvent("click", { x: 10, y: 20 }); // Правильно
handleEvent("mouseover", { target: document.getElementById("myElement")! }); // Правильно
handleEvent("keydown", { key: "Enter" }); // Правильно

handleEvent("click", { key: "Enter" }); // Помилка: Аргумент типу '{ key: string; }' не може бути присвоєний параметру типу '{ x: number; y: number; }'.

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

Найкращі практики використання літеральних типів

Для ефективного використання літеральних типів у ваших TypeScript проєктах, дотримуйтесь наступних найкращих практик:

Переваги використання літеральних типів

Висновок

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