Дізнайтеся, як використовувати відображені типи TypeScript для динамічної трансформації форм об'єктів, створюючи надійний та підтримуваний код для глобальних застосунків.
Відображені типи (Mapped Types) в TypeScript для динамічних трансформацій об'єктів: вичерпний посібник
TypeScript, з його сильним акцентом на статичній типізації, дозволяє розробникам писати більш надійний та підтримуваний код. Важливою функцією, яка значною мірою сприяє цьому, є відображені типи (mapped types). Цей посібник заглиблюється у світ відображених типів TypeScript, надаючи всебічне розуміння їх функціональності, переваг та практичного застосування, особливо в контексті розробки глобальних програмних рішень.
Розуміння основних концепцій
По суті, відображений тип дозволяє створювати новий тип на основі властивостей існуючого. Ви визначаєте новий тип, ітеруючи по ключах іншого типу та застосовуючи перетворення до значень. Це неймовірно корисно для сценаріїв, де потрібно динамічно змінювати структуру об'єктів, наприклад, змінювати типи даних властивостей, робити властивості необов'язковими або додавати нові властивості на основі існуючих.
Почнемо з основ. Розглянемо простий інтерфейс:
interface Person {
name: string;
age: number;
email: string;
}
Тепер визначимо відображений тип, який робить усі властивості Person
необов'язковими:
type OptionalPerson = {
[K in keyof Person]?: Person[K];
};
У цьому прикладі:
[K in keyof Person]
ітерує по кожному ключу (name
,age
,email
) інтерфейсуPerson
.?
робить кожну властивість необов'язковою.Person[K]
посилається на тип властивості в оригінальному інтерфейсіPerson
.
У результаті тип OptionalPerson
фактично виглядає так:
{
name?: string;
age?: number;
email?: string;
}
Це демонструє потужність відображених типів для динамічної модифікації існуючих типів.
Синтаксис та структура відображених типів
Синтаксис відображеного типу є досить специфічним і має таку загальну структуру:
type NewType = {
[Key in KeysType]: ValueType;
};
Розберемо кожен компонент:
NewType
: Назва, яку ви присвоюєте новому створюваному типу.[Key in KeysType]
: Це ядро відображеного типу.Key
— це змінна, яка ітерує по кожному членуKeysType
.KeysType
часто, але не завжди, єkeyof
іншого типу (як у нашому прикладі зOptionalPerson
). Це також може бути об'єднання рядкових літералів або більш складний тип.ValueType
: Це визначає тип властивості в новому типі. Це може бути прямий тип (наприклад,string
), тип, заснований на властивості оригінального типу (наприклад,Person[K]
), або більш складне перетворення оригінального типу.
Приклад: перетворення типів властивостей
Уявіть, що вам потрібно перетворити всі числові властивості об'єкта в рядки. Ось як це можна зробити за допомогою відображеного типу:
interface Product {
id: number;
name: string;
price: number;
quantity: number;
}
type StringifiedProduct = {
[K in keyof Product]: Product[K] extends number ? string : Product[K];
};
У цьому випадку ми:
- Ітеруємо по кожному ключу інтерфейсу
Product
. - Використовуємо умовний тип (
Product[K] extends number ? string : Product[K]
), щоб перевірити, чи є властивість числом. - Якщо це число, ми встановлюємо тип властивості на
string
; інакше — залишаємо оригінальний тип.
Отриманий тип StringifiedProduct
буде таким:
{
id: string;
name: string;
price: string;
quantity: string;
}
Ключові особливості та техніки
1. Використання keyof
та індексних сигнатур
Як було показано раніше, keyof
є фундаментальним інструментом для роботи з відображеними типами. Він дозволяє ітерувати по ключах типу. Індексні сигнатури надають спосіб визначення типу властивостей, коли ви не знаєте ключів заздалегідь, але все одно хочете їх трансформувати.
Приклад: перетворення всіх властивостей на основі індексної сигнатури
interface StringMap {
[key: string]: number;
}
type StringMapToString = {
[K in keyof StringMap]: string;
};
Тут усі числові значення в StringMap перетворюються на рядки в новому типі.
2. Умовні типи у відображених типах
Умовні типи — це потужна функція TypeScript, яка дозволяє виражати відношення між типами на основі умов. У поєднанні з відображеними типами вони дозволяють здійснювати дуже складні перетворення.
Приклад: видалення Null та Undefined з типу
type NonNullableProperties = {
[K in keyof T]: T[K] extends (null | undefined) ? never : T[K];
};
Цей відображений тип ітерує по всіх ключах типу T
і використовує умовний тип для перевірки, чи допускає значення null або undefined. Якщо так, то тип обчислюється як never, що фактично видаляє цю властивість; інакше він зберігає оригінальний тип. Такий підхід робить типи більш надійними, виключаючи потенційно проблематичні значення null або undefined, покращуючи якість коду та відповідаючи найкращим практикам для розробки глобального програмного забезпечення.
3. Утилітарні типи для ефективності
TypeScript надає вбудовані утилітарні типи, які спрощують поширені завдання маніпуляції типами. Ці типи використовують відображені типи "під капотом".
Partial
: Робить усі властивості типуT
необов'язковими (як показано в попередньому прикладі).Required
: Робить усі властивості типуT
обов'язковими.Readonly
: Робить усі властивості типуT
доступними лише для читання.Pick
: Створює новий тип лише із зазначеними ключами (K
) з типуT
.Omit
: Створює новий тип з усіма властивостями типуT
, крім зазначених ключів (K
).
Приклад: використання Pick
та Omit
interface User {
id: number;
name: string;
email: string;
role: string;
}
type UserSummary = Pick;
// { id: number; name: string; }
type UserWithoutEmail = Omit;
// { id: number; name: string; role: string; }
Ці утилітарні типи позбавляють вас необхідності писати повторювані визначення відображених типів та покращують читабельність коду. Вони особливо корисні у глобальній розробці для управління різними представленнями або рівнями доступу до даних на основі дозволів користувача або контексту програми.
Реальні застосунки та приклади
1. Валідація та трансформація даних
Відображені типи є неоціненними для валідації та перетворення даних, отриманих із зовнішніх джерел (API, бази даних, введення користувача). Це критично важливо у глобальних застосунках, де ви можете мати справу з даними з багатьох різних джерел і повинні забезпечити цілісність даних. Вони дозволяють визначати конкретні правила, такі як валідація типів даних, та автоматично змінювати структури даних на основі цих правил.
Приклад: перетворення відповіді API
interface ApiResponse {
userId: string;
id: string;
title: string;
completed: boolean;
}
type CleanedApiResponse = {
[K in keyof ApiResponse]:
K extends 'userId' | 'id' ? number :
K extends 'title' ? string :
K extends 'completed' ? boolean : any;
};
Цей приклад перетворює властивості userId
та id
(спочатку рядки з API) на числа. Властивість title
коректно типізується як рядок, а completed
залишається логічним значенням. Це забезпечує узгодженість даних та дозволяє уникнути потенційних помилок при подальшій обробці.
2. Створення пропсів для компонентів багаторазового використання
У React та інших UI-фреймворках відображені типи можуть спростити створення пропсів для компонентів багаторазового використання. Це особливо важливо при розробці глобальних UI-компонентів, які повинні адаптуватися до різних локалей та користувацьких інтерфейсів.
Приклад: обробка локалізації
interface TextProps {
textId: string;
defaultText: string;
locale: string;
}
type LocalizedTextProps = {
[K in keyof TextProps as `localized-${K}`]: TextProps[K];
};
У цьому коді новий тип, LocalizedTextProps
, додає префікс до назви кожної властивості TextProps
. Наприклад, textId
стає localized-textId
, що корисно для встановлення пропсів компонента. Цей патерн можна використовувати для генерації пропсів, які дозволяють динамічно змінювати текст залежно від локалі користувача. Це важливо для створення багатомовних користувацьких інтерфейсів, які бездоганно працюють у різних регіонах та мовах, наприклад, у застосунках для електронної комерції або міжнародних соціальних мережах. Трансформовані пропси надають розробнику більше контролю над локалізацією та можливість створювати послідовний користувацький досвід по всьому світу.
3. Динамічна генерація форм
Відображені типи корисні для динамічної генерації полів форми на основі моделей даних. У глобальних застосунках це може бути корисним для створення форм, які адаптуються до різних ролей користувачів або вимог до даних.
Приклад: автоматична генерація полів форми на основі ключів об'єкта
interface UserProfile {
firstName: string;
lastName: string;
email: string;
phoneNumber: string;
}
type FormFields = {
[K in keyof UserProfile]: {
label: string;
type: string;
required: boolean;
};
};
Це дозволяє визначати структуру форми на основі властивостей інтерфейсу UserProfile
. Це дозволяє уникнути необхідності вручну визначати поля форми, покращуючи гнучкість та підтримуваність вашого застосунку.
Просунуті техніки відображених типів
1. Перепризначення ключів (Key Remapping)
У TypeScript 4.1 було введено перепризначення ключів у відображених типах. Це дозволяє перейменовувати ключі під час трансформації типу. Це особливо корисно при адаптації типів до різних вимог API або коли ви хочете створити більш зручні для користувача назви властивостей.
Приклад: перейменування властивостей
interface Product {
productId: number;
productName: string;
productDescription: string;
price: number;
}
type ProductDto = {
[K in keyof Product as `dto_${K}`]: Product[K];
};
Це перейменовує кожну властивість типу Product
, додаючи на початку dto_
. Це цінно при відображенні між моделями даних та API, які використовують різні угоди про іменування. Це важливо в міжнародній розробці програмного забезпечення, де застосунки взаємодіють з кількома бекенд-системами, які можуть мати специфічні угоди про іменування, що забезпечує плавну інтеграцію.
2. Умовне перепризначення ключів
Ви можете поєднувати перепризначення ключів з умовними типами для більш складних перетворень, що дозволяє перейменовувати або виключати властивості на основі певних критеріїв. Ця техніка дозволяє здійснювати складні трансформації.
Приклад: виключення властивостей з DTO
interface Product {
id: number;
name: string;
description: string;
price: number;
category: string;
isActive: boolean;
}
type ProductDto = {
[K in keyof Product as K extends 'description' | 'isActive' ? never : K]: Product[K]
}
Тут властивості description
та isActive
фактично видаляються з генерованого типу ProductDto
, оскільки ключ розв'язується в never
, якщо властивість є 'description' або 'isActive'. Це дозволяє створювати специфічні об'єкти передачі даних (DTO), які містять лише необхідні дані для різних операцій. Така вибіркова передача даних є життєво важливою для оптимізації та конфіденційності в глобальному застосунку. Обмеження передачі даних гарантують, що через мережі надсилаються лише релевантні дані, зменшуючи використання пропускної здатності та покращуючи користувацький досвід. Це відповідає глобальним нормам конфіденційності.
3. Використання відображених типів з дженериками
Відображені типи можна поєднувати з дженериками для створення дуже гнучких та багаторазово використовуваних визначень типів. Це дозволяє писати код, який може обробляти різноманітні типи, значно підвищуючи повторне використання та підтримуваність вашого коду, що особливо цінно у великих проектах та міжнародних командах.
Приклад: узагальнена функція для перетворення властивостей об'єкта
function transformObjectValues(obj: T, transform: (value: T[K]) => U): {
[P in keyof T]: U;
} {
const result: any = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = transform(obj[key]);
}
}
return result;
}
interface Order {
id: number;
items: string[];
total: number;
}
const order: Order = {
id: 123,
items: ['apple', 'banana'],
total: 5.99,
};
const stringifiedOrder = transformObjectValues(order, (value) => String(value));
// stringifiedOrder: { id: string; items: string; total: string; }
У цьому прикладі функція transformObjectValues
використовує дженерики (T
, K
, та U
), щоб прийняти об'єкт (obj
) типу T
та функцію перетворення, яка приймає одну властивість з T і повертає значення типу U. Потім функція повертає новий об'єкт, який містить ті ж ключі, що й оригінальний об'єкт, але зі значеннями, які були перетворені до типу U.
Найкращі практики та рекомендації
1. Типізація та підтримуваність коду
Однією з найбільших переваг TypeScript та відображених типів є підвищена безпека типів. Визначаючи чіткі типи, ви виявляєте помилки на ранніх етапах розробки, зменшуючи ймовірність помилок під час виконання. Вони роблять ваш код легшим для розуміння та рефакторингу, особливо у великих проектах. Крім того, використання відображених типів гарантує, що код буде менш схильним до помилок у міру масштабування програмного забезпечення, адаптуючись до потреб мільйонів користувачів у всьому світі.
2. Читабельність та стиль коду
Хоча відображені типи можуть бути потужними, важливо писати їх у зрозумілій та читабельній манері. Використовуйте значущі імена змінних та коментуйте свій код, щоб пояснити мету складних перетворень. Чіткість коду гарантує, що розробники з різним досвідом зможуть читати та розуміти код. Послідовність у стилі, угодах про іменування та форматуванні робить код більш доступним і сприяє плавному процесу розробки, особливо в міжнародних командах, де різні члени працюють над різними частинами програмного забезпечення.
3. Надмірне використання та складність
Уникайте надмірного використання відображених типів. Хоча вони є потужними, вони можуть зробити код менш читабельним, якщо використовуються надмірно або коли доступні простіші рішення. Подумайте, чи не буде більш доцільним просте визначення інтерфейсу або проста утилітарна функція. Якщо ваші типи стають надто складними, їх може бути важко зрозуміти та підтримувати. Завжди враховуйте баланс між безпекою типів та читабельністю коду. Дотримання цього балансу гарантує, що всі члени міжнародної команди зможуть ефективно читати, розуміти та підтримувати кодову базу.
4. Продуктивність
Відображені типи в основному впливають на перевірку типів під час компіляції і зазвичай не створюють значних накладних витрат на продуктивність під час виконання. Однак надто складні маніпуляції з типами потенційно можуть сповільнити процес компіляції. Мінімізуйте складність і враховуйте вплив на час збирання, особливо у великих проектах або для команд, що працюють у різних часових поясах та з різними обмеженнями ресурсів.
Висновок
Відображені типи в TypeScript пропонують потужний набір інструментів для динамічної трансформації форм об'єктів. Вони є неоціненними для створення типобезпечного, підтримуваного коду, який можна використовувати повторно, особливо при роботі зі складними моделями даних, взаємодією з API та розробкою UI-компонентів. Опанувавши відображені типи, ви зможете писати більш надійні та адаптивні застосунки, створюючи краще програмне забезпечення для глобального ринку. Для міжнародних команд та глобальних проектів використання відображених типів забезпечує високу якість та підтримуваність коду. Обговорені тут функції є вирішальними для створення адаптивного та масштабованого програмного забезпечення, покращення підтримуваності коду та створення кращого досвіду для користувачів по всьому світу. Відображені типи полегшують оновлення коду при додаванні або зміні нових функцій, API або моделей даних.