Mapped Types – це механізм TypeScript, який дозволяє створювати нові типи на основі вже існуючих, змінюючи їх властивості за певними правилами. Це зручно, коли потрібно створити похідний тип від існуючого інтерфейсу або типу, не дублюючи його структуру вручну.

Типові сценарії використання Mapped Types:

  • робити властивості необов’язковими або обов’язковими
  • позначати властивості як readonly
  • змінювати тип значень властивостей
  • перейменовувати ключі за допомогою шаблонних літералів

Базові приклади

1. Зробити властивості необов’язковими:

type User = {
  id: number;
  name: string;
  email: string;
};

type PartialUser = {
  [P in keyof User]?: User[P];
};

const u1: PartialUser = { id: 1 }; // ок
const u2: PartialUser = {}; // ok
const u3: PartialUser = { id: 2, name: 'Alice' }; // ok

[P in keyof User]? перебирає всі ключі типу User та робить кожну властивість необов’язковою.

Те саме можна зробити простіше – за допомогою вбудованого типу Partial<T>:

type PartialUser = Partial<User>;

2. Позначити властивості як readonly:

type User = {
  id: number;
  name: string;
  email: string;
};

type ReadonlyUser = {
  readonly [P in keyof User]: User[P];
};

const user: ReadonlyUser = { id: 1, name: 'Alice', email: 'alice@example.com' };
user.id = 2; // Error: Cannot assign to 'id' because it is a read-only property

Ключове слово readonly робить усі властивості незмінними після створення об’єкта.

Аналогічний ефект дає вбудований тип Readonly<T>:

type ReadonlyUser = Readonly<User>;

3. Дозволити null як значення:

type Product = {
  name: string;
  price: number;
  inStock: boolean;
};

type NullableProduct = {
  [P in keyof Product]: Product[P] | null;
};

const product: NullableProduct = { name: 'Laptop', price: null, inStock: true };

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

4. Перейменування властивостей:

type Person = {
  firstName: string;
  lastName: string;
};

type PrefixedPerson = {
  [P in keyof Person as `person${Capitalize<P>}`]: Person[P];
};

const p: PrefixedPerson = { personFirstName: 'John', personLastName: 'Doe' };

Тут ми використовуємо шаблонні літерали та утиліту Capitalize, щоб додати префікс до назв властивостей.

Додаткові приклади

1. Комбінований мапінг

Ми можемо поєднувати кілька трансформацій одночасно. Наприклад, зробити всі властивості одночасно необов’язковими і тільки для читання:

type User = {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
};

type OptionalReadonly<T> = {
  readonly [P in keyof T]?: T[P];
};

type UserOptionalReadonly = OptionalReadonly<User>;

const user: UserOptionalReadonly = { id: 1, name: 'Alice' };
user.name = 'Bob'; // Error: Cannot assign to 'name' because it is a read-only property

Усі властивості User тепер одночасно опціональні та readonly.

2. Зміна типу властивостей

Mapped Types дозволяють змінювати тип кожного значення – наприклад, зробити всі властивості рядковими:

type User = {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
};

type Stringified<T> = {
  [P in keyof T]: string;
};

type StringifiedUser = Stringified<User>;

const strUser: StringifiedUser = {
  id: '1', // number → string
  name: 'Alice', // string → string
  email: 'a@ex.com', // string → string
  isActive: 'true', // boolean → string
};

3. Вибірковий мапінг (Pick та Omit)

Вбудовані утиліти Pick<T, K> та Omit<T, K> теж базуються на mapped types. Вони дозволяють вибирати або виключати певні властивості з типу:

type User = {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
};

// Спрощена реалізація утиліти Pick
type PickImpl<T, K extends keyof T> = {
  [P in K]: T[P];
};

// Спрощена реалізація утиліти Omit
type OmitImpl<T, K extends keyof any> = PickImpl<T, Exclude<keyof T, K>>;

PickImpl – обирає лише певні властивості:

type UserIdName = PickImpl<User, 'id' | 'name'>;
// еквівалентно:
type UserIdNameManual = { id: number; name: string };

const u1: UserIdName = { id: 1, name: 'Alice' };
u1.email = 'a@ex.com'; 
// Error: Property 'email' does not exist on type 'UserIdName'

OmitImpl – виключає певні властивості:

type UserWithoutEmail = OmitImpl<User, 'email'>;
// еквівалентно:
type UserWithoutEmailManual = { id: number; name: string; isActive: boolean };

const u2: UserWithoutEmail = { id: 2, name: 'Bob', isActive: true };
u2.email = 'b@ex.com'; 
// Error: Property 'email' does not exist on type 'UserWithoutEmail'

Пояснення:

  • Pick<T, K> створює новий тип, який містить лише властивості K з T.
  • Omit<T, K> робить протилежне – створює тип без зазначених властивостей K.

Рекомендації щодо використання Mapped Types

  • Не створюйте надто складні конструкції: Mapped Types мають бути зрозумілими з першого погляду. Якщо тип перетворюється у головоломку – розбийте його на кілька менших або використайте проміжні типи.
  • Використовуйте готові утиліти: У TypeScript вже є набір перевірених утиліт – Partial, Readonly, Pick, Omit та інші. Вони покривають більшість типових сценаріїв і роблять код більш читабельним.
  • За потреби комбінуйте кілька Mapped Types: Це дає можливість отримати більш гнучкі варіанти (наприклад, Partial + Readonly).
  • Використовуйте перейменування ключів (as) і шаблонні літерали типів: Це дозволяє створювати більш виразні назви властивостей, наприклад з префіксами або модифікаціями.

Підсумок

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

Якщо ви вже користуєтесь вбудованими утилітами TypeScript на кшталт Partial, Readonly, Pick або Omit – ви фактично вже застосовуєте Mapped Types у зручній, готовій до використання формі.