#16 - Маппінг типів та створення типів на основі існуючих, глибинна типізація обʼєктів

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

Маппінг типів

По факту це шаблон для створення нового типу, перебираючи властивості іншого типу.

type MyMappedType<T> = {
  [K in keyof T]: T[K];
};

Цей шаблон означає: «для кожного ключа K з типу T, взяти тип T[K]». Як це можна використати, ну наприклад - зробити всі поля обовʼязковими:

type User = {
  name?: string;
  age?: number;
};

type RequiredUser = {
  [K in keyof User]-?: User[K];
};

Створення типів на основі існуючих

TypeScript вже має вбудовані утилітарні типи, які базуються на маппінгу (ми вже їх робирали в попередніх уроках):

  • Partial<T> - Робить усі властивості T необовʼязковими
  • Required<T> - Робить усі властивості T обовʼязковими
  • Readonly<T> - Робить усі властивості T readonly
  • Pick<T, K> - Обирає підмножину властивостей
  • Omit<T, K> - Виключає певні властивості
  • Record<K, T> - Створює тип з ключами K і значеннями типу T
type Roles = 'admin' | 'user' | 'guest';
type RoleAccess = Record<Roles, boolean>;
// RoleAccess = { admin: boolean, user: boolean, guest: boolean }

Глибинна типізація обʼєктів

Іноді нам потрібно рекурсивно типізувати або модифікувати вкладені обʼєкти. Це вже вимагає рекурсивного маппінгу.

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

На прикладі реального коду:

type Config = {
  db: {
    host: string;
    port: number;
  };
  logging: boolean;
};

const config: DeepReadonly<Config> = {
  db: { host: 'localhost', port: 3306 },
  logging: true,
};

// config.db.port = 5432 Помилка — властивість readonly

Ще один приклад, якщо вам потрібно змінити всі типи на string (глибоко)

type DeepStringify<T> = {
  [K in keyof T]: T[K] extends object
    ? DeepStringify<T[K]>
    : string;
};

// --- 

type Data = {
  id: number;
  info: {
    age: number;
    active: boolean;
  };
};

type Stringified = DeepStringify<Data>;
/*
{
  id: string;
  info: {
    age: string;
    active: string;
  };
}
*/

Типізація глибокого доступу

Іноді хочемо мати тип, що представляє всі “шляхи” до полів:

type Path<T, Prev extends string = ''> = {
  [K in keyof T]: T[K] extends object
    ? `${Prev}${K & string}` | Path<T[K], `${Prev}${K & string}.`>
    : `${Prev}${K & string}`;
}[keyof T];

// -- 

type User = {
  name: string;
  address: {
    city: string;
    zip: number;
  };
};

type UserPaths = Path<User>; // "name" | "address.city" | "address.zip"