← Усі статті

Як обмеження узагальнених типів TypeScript запобігають помилкам

Як `extends`, `keyof` та індексний доступ допомагають TypeScript знайти хибні ключі й значення до запуску застосунку.

Як обмеження узагальнених типів TypeScript запобігають помилкам
Зміст

Коротко

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

Вихідний матеріал пояснює зв’язку extends, keyof та індексного доступу T[K]. Вона перетворює припущення про форму даних на правило, яке перевіряє компілятор: неправильний ключ або значення не потрапить до зібраного застосунку.

Що сталося

Автор розглядає звичну допоміжну функцію get<T>(obj: T, key: string). Вона видається універсальною, проте key може бути довільним текстом, а T не обіцяє наявності жодної властивості.

Обмеження T extends SomeType визначає мінімальну форму аргументу. Наприклад, T extends { id: string } означає, що всередині функції можна безпечно звертатися до item.id, а число чи об’єкт без id не пройде перевірку типів.

Оператор keyof утворює об’єднання допустимих назв властивостей. Для User із полями id та name це буде "id" | "name", і набір автоматично змінюватиметься разом з описом типу.

Разом вони формують поширений запис:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

Тепер K може бути лише ключем T, а T[K] зберігає точний тип обраного поля. Для id результатом буде число, а для name — рядок.

Чому це важливо

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

Перевірка на етапі компіляції наближає зворотний зв’язок до місця помилки. Редактор одразу позначить getProperty(user, "age"), якщо об’єкт не має age, замість пошуку причини збою за журналами після розгортання.

Точність важлива і під час оновлення даних. У updateProperty<T, K extends keyof T>(obj, key, value: T[K]) не вийде записати true у числове поле port: тип значення пов’язаний саме з вибраним ключем.

Втім, типи не замінюють перевірку даних із мережі. TypeScript не змінює JavaScript під час виконання; зовнішній JSON і далі слід перевіряти схемою або явними умовами на межі застосунку.

На практиці

Спочатку визначте, які властивості справді використовує функція. Якщо їй потрібен лише ідентифікатор, обмеження T extends { id: number } корисніше за вимогу повного типу User із десятками не пов’язаних полів.

Для доступу до поля за назвою застосовуйте пару T і K extends keyof T. Вона доречна в утилітах форм, стану та побудови запитів, де важливо не втратити зв’язок між ключем і значенням.

  1. Для необов’язкових полів враховуйте undefined: ключ email? входить до keyof User, але T["email"] може не містити рядка.
  2. За наявності індексних сигнатур за потреби звужуйте правило до K extends keyof T & string.
  3. Не використовуйте надто широке T extends object, якщо функція залежить від конкретних властивостей.
  4. Окремо перевіряйте зовнішній JSON, а вже потім передавайте підтверджені дані до строго типізованих функцій.

На цих самих правилах побудовані умовні та відображувані типи. Запис { [K in keyof T]: T[K] | null } створює нову форму з тими самими ключами, а T extends U ? X : Y обирає результат за умовою.

Підсумок

Зв’язка extends і keyof — не формальність: вона фіксує мінімальні вимоги функції та зберігає конкретний тип результату. Патерн <T, K extends keyof T> варто використовувати всюди, де код читає або змінює поля за назвою.

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