Утилитарные типы: Record, Readonly, Exclude, Extract
Оставшиеся встроенные типы для трансформации типов
Цели урока
- Разобрать утилитарный тип Record<K, V>
- Разобрать утилитарный тип Readonly<T>
- Разобрать утилитарный тип Exclude<T, U>
- Разобрать утилитарный тип Extract<T, U>
- Научиться комбинировать все утилитарные типы
Record<K, V> — объект с ключами и значениями
Record<K, V> создаёт тип объекта, где ключи имеют тип K, а значения — тип V:
type UserRoles = Record<string, boolean>;
// Эквивалентно:
// type UserRoles = { [key: string]: boolean };
// Использование:
const roles: UserRoles = {
admin: true,
user: true,
guest: false
};
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> создаёт тип, где все свойства доступны только для чтения. Их нельзя изменить после создания:
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:
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:
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: сравнение
Давайте сравним эти два типа, чтобы понять разницу:
type A = string | number | boolean; type B = Exclude<A, string>; // B = number | boolean
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:
// Record<string, number> — тип объекта
// Object — базовый класс всех объектов
// Используйте Record для типизации словарей
// const obj: Readonly<{ a: number }> = { a: 1 };
// obj.a = 2; // Ошибка компиляции!
// Но Object.freeze делает это и во время выполнения
// Exclude<string, number> = string (ничего не исключено)
// Extract<string, number> = never (ничего не извлечено)
// Они полезны для union-типов, не для примитивов
Поздравляем!
Вы завершили модуль 6: Generics. Теперь вы знаете, как создавать обобщённые функции, интерфейсы и классы, использовать ограничения и утилитарные типы.
Тест по утилитарным типам Record, Readonly, Exclude, Extract
8 вопросов