Пошук уроків, статей та іншого контенту
Використовуємо keyof разом із Generics для типобезпечного доступу до властивостей об’єктів.
keyofОператор keyof отримує об’єднання ключів типу об’єкта.
type User = {
id: number;
name: string;
isActive: boolean;
};
type UserKey = keyof User;
// "id" | "name" | "isActive"UserKey може містити лише назви властивостей, які справді існують у User:
const validKey: UserKey = "name";
// Помилка: властивості "email" немає в типі User
// const invalidKey: UserKey = "email";Тип ключа особливо корисний у функціях, які працюють із різними об’єктами.
Розглянемо функцію для отримання властивості:
function getProperty(object: object, key: string) {
return object[key];
}Такий код не є типобезпечним: тип object не гарантує, що в ньому існує властивість із переданим рядком. Крім того, функція не зберігає точний тип результату.
Наприклад, для користувача хочеться отримати:
number, якщо ключ — "id";
string, якщо ключ — "name";
boolean, якщо ключ — "isActive".
Для цього потрібні Generics разом із keyof.
keyofСинтаксис типобезпечної функції доступу до властивості:
function getProperty<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}Тут:
T — тип об’єкта;
K — тип ключа;
K extends keyof T означає, що K може бути лише ключем типу T;
T[K] — тип значення властивості з ключем K.
Розглянемо повний приклад:
type User = {
id: number;
name: string;
isActive: boolean;
};
function getProperty<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
const user: User = {
id: 42,
name: "Олена",
isActive: true,
};
const id = getProperty(user, "id");
const name = getProperty(user, "name");
const isActive = getProperty(user, "isActive");
console.log(id); // 42
console.log(name); // Олена
console.log(isActive); // true
// TypeScript визначає типи:
// id: number
// name: string
// isActive: boolean
// Помилка: "email" не є ключем типу User
// const email = getProperty(user, "email");TypeScript виводить тип T з об’єкта user, а тип K — із ключа, який передано другим аргументом.
T[K]Запис T[K] називають indexed access type — типом доступу за індексом.
type Product = {
title: string;
price: number;
inStock: boolean;
};
type ProductTitle = Product["title"];
// string
type ProductPrice = Product["price"];
// number
type ProductField = Product[keyof Product];
// string | number | booleanЯкщо K — конкретний ключ, T[K] повертає тип відповідної властивості:
function getProductTitle(product: Product): Product["title"] {
return product.title;
}У Generic-функції ключ K може бути різним, тому T[K] автоматично підлаштовується під нього.
Ту саму техніку можна застосувати до функції, яка змінює значення властивості:
function setProperty<T, K extends keyof T>(
object: T,
key: K,
value: T[K],
): void {
object[key] = value;
}
type Settings = {
theme: "light" | "dark";
fontSize: number;
notificationsEnabled: boolean;
};
const settings: Settings = {
theme: "light",
fontSize: 16,
notificationsEnabled: true,
};
setProperty(settings, "theme", "dark");
setProperty(settings, "fontSize", 18);
setProperty(settings, "notificationsEnabled", false);
console.log(settings);
// Помилка: значення має бути одним із "light" | "dark"
// setProperty(settings, "theme", "blue");
// Помилка: для fontSize очікується number
// setProperty(settings, "fontSize", "large");
// Помилка: такого ключа немає в Settings
// setProperty(settings, "language", "uk");Обмеження T[K] пов’язує ключ і значення:
для "theme" дозволені лише "light" або "dark";
для "fontSize" потрібне число;
для "notificationsEnabled" потрібне логічне значення.
Саме цей зв’язок неможливо надійно описати, якщо ключ і значення мають просто типи string та unknown.
Зазвичай типи не потрібно передавати вручну:
const product = {
name: "Ноутбук",
price: 35000,
};
function getValue<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
const productName = getValue(product, "name");
// string
const productPrice = getValue(product, "price");
// numberTypeScript визначає:
T = {
name: string;
price: number;
}
K = "name" | "price"Для конкретного виклику K звужується до фактично переданого ключа. Тому productName має тип string, а productPrice — тип number.
Якщо ключ зберігається у змінній, її тип також має бути сумісним із keyof об’єкта:
type Article = {
title: string;
views: number;
};
const article: Article = {
title: "TypeScript Generics",
views: 1200,
};
const key: keyof Article = "title";
const value = getProperty(article, key);
console.log(value);Тип value у цьому випадку:
string | numberЦе логічно: змінна key може містити "title" або "views", тому результат може бути рядком або числом.
Якщо ключ відомий як конкретне значення, TypeScript може зберегти точніший тип:
const titleKey = "title" as const;
const title = getProperty(article, titleKey);
// stringkeyof typeof для значеньkeyof застосовується до типів, а не безпосередньо до значень. Якщо потрібно отримати ключі вже створеного об’єкта, використовується typeof:
const statuses = {
draft: "Чернетка",
published: "Опубліковано",
archived: "Архів",
};
type StatusKey = keyof typeof statuses;
// "draft" | "published" | "archived"
function getStatusLabel(key: StatusKey): string {
return statuses[key];
}
console.log(getStatusLabel("draft"));
console.log(getStatusLabel("published"));
// Помилка: такого ключа немає в об'єкті
// getStatusLabel("deleted");У виразі keyof typeof statuses:
typeof statuses отримує тип об’єкта statuses;
keyof отримує об’єднання його ключів.
Іноді потрібно отримати з об’єкта лише значення властивостей із певними ключами:
function pick<T, K extends keyof T>(object: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
for (const key of keys) {
result[key] = object[key];
}
return result;
}
const user = {
id: 7,
name: "Андрій",
isAdmin: false,
};
const publicData = pick(user, ["id", "name"]);
console.log(publicData);
// { id: 7, name: "Андрій" }У цій функції:
T — тип усього об’єкта;
K extends keyof T — ключі, які можна вибрати;
Pick<T, K> — вбудований Utility Type, що створює тип лише з вибраних властивостей.
TypeScript перевіряє і ключі:
// Помилка: "password" не є ключем типу об'єкта
// const data = pick(user, ["id", "password"]);string замість keyof Tfunction unsafeGet<T>(object: T, key: string) {
// Такий підхід не гарантує, що key є ключем T
return (object as Record<string, unknown>)[key];
}Приведення типу приховує проблему від TypeScript. Якщо ключ має бути властивістю об’єкта, краще використати:
function safeGet<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}keyof anyТип keyof any фактично дозволяє string | number | symbol, але не пов’язує ключ із конкретним об’єктом:
function notSpecific<T, K extends keyof any>(object: T, key: K) {
// Зв'язок між K та ключами T відсутній
return object[key];
}Потрібно обмежувати ключ саме через keyof T:
function specific<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}Якщо ключ має широкий тип string, TypeScript не може гарантувати, що він належить об’єкту:
const key: string = "name";
// Помилка: тип string не гарантовано є ключем Article
// getProperty(article, key);Потрібно явно описати допустимі ключі:
const key: keyof Article = "name";
const result = getProperty(article, key);Небезпечний варіант:
function badSet<T, K extends keyof T, V>(
object: T,
key: K,
value: V,
): void {
// V не пов'язаний із типом властивості T[K]
}Правильніше використовувати T[K]:
function goodSet<T, K extends keyof T>(
object: T,
key: K,
value: T[K],
): void {
object[key] = value;
}keyof T створює об’єднання ключів типу T.
K extends keyof T гарантує, що Generic K є ключем об’єкта T.
T[K] отримує тип значення властивості за ключем K.
Комбінація T, K extends keyof T і T[K] зберігає зв’язок між об’єктом, ключем і значенням.
keyof typeof object дає змогу отримати ключі конкретного об’єкта.
Такий підхід корисний для функцій читання, зміни та вибору властивостей без небезпечних приведень типів.