$ sudo teach IT
МОДУЛЬ 6 · УРОК 6

Утилитарные типы: Record, Readonly, Exclude, Extract

Оставшиеся встроенные типы для трансформации типов

Цели урока

  • Разобрать утилитарный тип Record<K, V>
  • Разобрать утилитарный тип Readonly<T>
  • Разобрать утилитарный тип Exclude<T, U>
  • Разобрать утилитарный тип Extract<T, U>
  • Научиться комбинировать все утилитарные типы

Record<K, V> — объект с ключами и значениями

Record<K, V> создаёт тип объекта, где ключи имеют тип K, а значения — тип V:

// Record создаёт объект с определёнными ключами и значениями
type UserRoles = Record<string, boolean>;
// Эквивалентно:
// type UserRoles = { [key: string]: boolean };

// Использование:
const roles: UserRoles = {
  admin: true,
  user: true,
  guest: false
};

Record удобен для:

  • Словарей и маппингов
  • Статусов и флагов
  • Конфигураций с известными ключами
  • Группировки данных по ключам
// Record с литеральными типами ключей
type HttpStatus = Record<
  "ok" | "error" | "loading",
  string
>;

const messages: HttpStatus = {
  ok: "Успешно",
  error: "Ошибка",
  loading: "Загрузка..."
};

// Record с интерфейсом в качестве значений
interface Product {
  name: string;
  price: number;
}

type ProductCatalog = Record<string, Product>;

const catalog: ProductCatalog = {
  laptop: { name: "Ноутбук", price: 50000 },
  phone: { name: "Телефон", price: 30000 },
  tablet: { name: "Планшет", price: 25000 }
};

Readonly<T> — неизменяемость свойств

Readonly<T> создаёт тип, где все свойства доступны только для чтения. Их нельзя изменить после создания:

// Readonly делает все свойства неизменяемыми
interface Point {
  x: number;
  y: number;
}

type ReadonlyPoint = Readonly<Point>;

// Использование:
const origin: ReadonlyPoint = { x: 0, y: 0 };

// Ошибка: свойство доступно только для чтения!
// origin.x = 10; // Ошибка компиляции!

// Readonly для массивов:
type ReadonlyArray<T> = Readonly<T[]>;

const colors: ReadonlyArray<string> = ["red", "green", "blue"];
// colors.push("yellow"); // Ошибка!
// colors[0] = "purple"; // Ошибка!

Readonly удобен для:

  • Константных данных (конфиги, настройки)
  • Неизменяемых структур (графы, деревья)
  • Иммутабельных состояний (Redux-стайл)
  • Защиты от случайных изменений

Exclude<T, U> — исключение типов

Exclude<T, U> из union-типа T исключает все типы, которые присутствуют в U:

// Exclude исключает типы из union
type AllTypes = string | number | boolean | null;

// Исключаем null:
type NonNullableTypes = Exclude<AllTypes, null>;
// string | number | boolean

// Исключаем string:
type NonStringTypes = Exclude<AllTypes, string>;
// number | boolean | null

// Исключаем несколько типов:
type PrimitiveTypes = Exclude<AllTypes, null | boolean>;
// string | number

Exclude удобен для:

  • Создания подмножеств union-типов
  • Исключения нежелательных вариантов
  • Фильтрации типов в условных типах

Extract<T, U> — извлечение типов

Extract<T, U> — противоположность Exclude. Он извлекает из union-типа T только те типы, которые присутствуют в U:

// Extract извлекает совпадающие типы
type AllTypes = string | number | boolean | null;

// Извлекаем только string и number:
type StringAndNumber = Extract<AllTypes, string | number>;
// string | number

// Извлекаем только boolean:
type OnlyBoolean = Extract<AllTypes, boolean>;
// boolean

// Если ничего не совпадает — пустой тип:
type Nothing = Extract<AllTypes, undefined>;
// never (пустой тип)

Extract удобен для:

  • Извлечения нужных вариантов из union-типа
  • Фильтрации типов
  • Работы с литеральными типами

Exclude vs Extract: сравнение

Давайте сравним эти два типа, чтобы понять разницу:

Exclude<T, U> — исключает:
type A = string | number | boolean;
type B = Exclude<A, string>;
// B = number | boolean
Extract<T, U> — извлекает:
type A = string | number | boolean;
type B = Extract<A, string>;
// B = string

Практический пример: система статусов

Покажем, как Record и Readonly работают вместе:

// Система статусов задач
type TaskStatus = "todo" | "in_progress" | "done";

// Карта статусов с описаниями
const statusLabels: Record<TaskStatus, string> = {
  todo: "К выполнению",
  in_progress: "В процессе",
  done: "Выполнено"
};

// Readonly версия (нельзя изменить)
const statuses: Readonly<Record<TaskStatus, string>> = {
  todo: "todo",
  in_progress: "in_progress",
  done: "done"
};

// Ошибка: нельзя изменить статус!
// statuses.todo = "completed"; // Ошибка!

Практический пример: фильтрация типов

Exclude и Extract удобны для фильтрации union-типов:

// Типы действий пользователя
type Action =
  | { type: "LOGIN"; userId: number }
  | { type: "LOGOUT"; userId: number }
  | { type: "UPDATE"; userId: number; data: any }
  | { type: "DELETE"; userId: number };

// Извлекаем только действия с userId:
type ActionWithUser = Extract<Action, { userId: number }>;
// Все действия, потому что все имеют userId

// Исключаем DELETE:
type NonDeleteAction = Exclude<Action, { type: "DELETE" }>;
// LOGIN | LOGOUT | UPDATE

// Извлекаем типы action по строковому литералу:
type LoginAction = Extract<Action, { type: "LOGIN" }>;
// { type: "LOGIN"; userId: number }

Практический пример: конфигурация приложения

Покажем, как Record и Readonly работают в реальном проекте:

// Конфигурация приложения
interface AppConfig {
  apiUrl: string;
  timeout: number;
  debug: boolean;
  version: string;
}

// Конфиг по умолчанию (Readonly)
const defaultConfig: Readonly<AppConfig> = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  debug: false,
  version: "1.0.0"
};

// Частичная конфигурация для переопределения
function createConfig(
  overrides: Partial<AppConfig>
): AppConfig {
  return { ...defaultConfig, ...overrides };
}

// Переопределяем только нужные поля:
const devConfig = createConfig({
  debug: true,
  timeout: 1000
});

Все утилитарные типы: сводная таблица

Тип Описание Пример
Partial<T> Все свойства необязательны { name?: string }
Required<T> Все свойства обязательны { name: string }
Pick<T, K> Только выбранные свойства Pick<User, 'name'>
Omit<T, K> Все свойства кроме исключённых Omit<User, 'id'>
Record<K, V> Объект с ключами K и значениями V Record<string, number>
Readonly<T> Все свойства доступны только для чтения Readonly<Config>
Exclude<T, U> Исключает типы U из union T Exclude<A | B, A> = B
Extract<T, U> Извлекает типы U из union T Extract<A | B, A> = A

Итоги урока

  • Record<K, V> — создаёт объект с определёнными ключами и значениями
  • Readonly<T> — делает все свойства неизменяемыми
  • Exclude<T, U> — исключает типы U из union-типа T
  • Extract<T, U> — извлекает типы U из union-типа T
  • Record удобен для словарей, конфигураций, статусов
  • Readonly удобен для иммутабельных данных
  • Exclude и Extract — противоположные операции для union-типов
  • Все утилитарные типы можно комбинировать

Практический пример: типобезопасный event emitter

Создадим типобезопасную систему событий с использованием Record и Readonly:

// Карта событий и их данных
interface EventMap {
  userCreated: { id: number; name: string };
  userDeleted: { id: number };
  orderPlaced: { orderId: number; total: number };
}

// Тип для обработчика события
type EventHandler<T> = (data: T) => void;

// Тип для хранилища обработчиков
type EventHandlers<T extends Record<string, any>> = {
  -readonly [K in keyof T]: EventHandler<T[K]>[];
};

// Класс EventEmitter
class TypedEventEmitter<T extends Record<string, any>> {
  private handlers: EventHandlers<T> = Object.create(null);

  on<K extends keyof T>(
    event: K,
    handler: EventHandler<T[K]>
  ): void {
    if (!this.handlers[event]) {
      this.handlers[event] = [];
    }
    this.handlers[event].push(handler);
  }

  emit<K extends keyof T>(
    event: K,
    data: T[K]
  ): void {
    // Вызываем обработчики
  }
}

// Использование:
const emitter = new TypedEventEmitter<EventMap>();

// OK! Типы совпадают:
emitter.on("userCreated", (data) => {
  console.log(data.name); // OK! data.name — string
});

emitter.emit("userCreated", { id: 1, name: "Алиса" }); // OK!

Практический пример: типизированный reducer

Создадим типизированный reducer с использованием Record и Exclude:

// Типы действий
type Action =
  | { type: "INCREMENT" }
  | { type: "DECREMENT" }
  | { type: "SET"; value: number }
  | { type: "RESET" };

// Исключаем SET (требует дополнительные данные)
type SimpleAction = Exclude<Action, { type: "SET" }>;

// Извлекаем только INCREMENT и DECREMENT
type ToggleAction = Extract<Action, { type: "INCREMENT" | "DECREMENT" }>;

// Record для маппинга действий
type ActionHandler = (state: number, action: any) => number;
type ActionHandlers = Record<Action["type"], ActionHandler>;

// Обработчики
const handlers: ActionHandlers = {
  INCREMENT: (state) => state + 1,
  DECREMENT: (state) => state - 1,
  SET: (state, action) => action.value,
  RESET: () => 0
};

Практический пример: Readonly для иммутабельных данных

Покажем, как Readonly защищает данные от случайных изменений:

// Конфигурация (нельзя изменить)
const config = Object.freeze({
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3
}) as Readonly<{
  apiUrl: string;
  timeout: number;
  retries: number;
}>;

// Readonly для вложенных объектов
interface AppState {
  user: {
    name: string;
    age: number;
  };
  settings: {
    theme: string;
    language: string;
  };
}

// Глубокое Readonly (вложенные объекты тоже неизменяемы)
type DeepReadonly<T> = Readonly<{
  [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
}>;

// Использование:
const state: DeepReadonly<AppState> = {
  user: { name: "Алиса", age: 25 },
  settings: { theme: "dark", language: "ru" }
};

// Ошибка: нельзя изменить!
// state.user.name = "Борис"; // Ошибка!
// state.settings.theme = "light"; // Ошибка!

Распространённые ошибки

Вот частые ошибки при использовании Record, Readonly, Exclude, Extract:

Ошибка 1: Record vs Object
// Record создаёт тип, Object — это класс
// Record<string, number> — тип объекта
// Object — базовый класс всех объектов
// Используйте Record для типизации словарей
Ошибка 2: Readonly не защищает от мутаций
// Readonly защищает только на уровне типов!
// const obj: Readonly<{ a: number }> = { a: 1 };
// obj.a = 2; // Ошибка компиляции!
// Но Object.freeze делает это и во время выполнения
Ошибка 3: Exclude/Extract с не-union типами
// Exclude и Extract работают с union-типами
// Exclude<string, number> = string (ничего не исключено)
// Extract<string, number> = never (ничего не извлечено)
// Они полезны для union-типов, не для примитивов

Поздравляем!

Вы завершили модуль 6: Generics. Теперь вы знаете, как создавать обобщённые функции, интерфейсы и классы, использовать ограничения и утилитарные типы.

Тест по утилитарным типам Record, Readonly, Exclude, Extract

8 вопросов

Роли пользователей

Premium