Повторное использование типов
Как выводить связанные модели из общей: readonly, Pick, Partial, Record и обобщённые функции.
On this page
Одно мероприятие появляется в каталоге, карточке и форме редактирования. Если вручную повторять одни и те же свойства в нескольких типах, при изменении модели легко что-то забыть. TypeScript позволяет получить нужные описания из уже существующего.
Ниже используем одну небольшую модель:
type EventItem = {
readonly id: number;
name: string;
price: number;
seatsLeft: number;
};Что можно менять
readonly id запрещает присваивать новый идентификатор через значение типа EventItem. Название и цену менять можно:
Продолжение модели выше.
const event: EventItem = {
id: 1,
name: "Кинопоказ",
price: 300,
seatsLeft: 8,
};
event.price = 250;
// event.id = 2; // Ошибка: id доступен только для чтения.Это ограничение проверки типов, а не заморозка объекта при выполнении. Оно помогает обозначить намерение: идентификатор сохраняется, редактируемые данные меняются.
Если функция должна только читать список, можно принять readonly EventItem[]. Она сможет пройти по массиву и отфильтровать его, но не сможет вызвать у него push или sort. При этом свойства самих объектов автоматически не становятся неизменяемыми:
function getNames(items: readonly EventItem[]): string[] {
return items.map((item) => item.name);
}Для карточки нужна только часть полей
Pick выбирает свойства из существующего типа:
type EventPreview = Pick<EventItem, "id" | "name" | "price">;
function formatPreview(item: EventPreview): string {
return `${item.name}: ${item.price} ₽`;
}Читайте запись так: «возьми из EventItem поля id, name и price». Если тип цены в общей модели изменится, EventPreview тоже получит это изменение.
Тип не вырезает данные из настоящего объекта. Функция может получить полный EventItem, но для своей работы требует только выбранные поля. Если нужно отправить меньше данных в сеть, отдельно создают объект с этими полями.
Обратная операция – Omit<EventItem, "id">: получить описание без указанного свойства. Например, новый объект ещё не получил идентификатор. Pick удобен, когда важен точный список нужных полей; Omit – когда проще перечислить исключения.
Частичное редактирование
При изменении названия не хочется заново передавать цену и количество мест. Сначала выберем редактируемые поля, затем сделаем их необязательными:
type EventChanges = Partial<Pick<EventItem, "name" | "price">>;
function updateEvent(item: EventItem, changes: EventChanges): EventItem {
return {
...item,
name: changes.name ?? item.name,
price: changes.price ?? item.price,
};
}
const renamed = updateEvent(event, { name: "Вечер кино" });
console.log(event.name); // "Кинопоказ": исходный объект не изменён.
console.log(renamed.name); // "Вечер кино"Partial добавляет возможность не передавать каждое поле выбранной модели. Вместе эти инструменты описывают: «можно обновить название, цену или оба поля». Пустое изменение {} тоже допустимо. Если по условиям задачи нужен хотя бы один изменяемый параметр, это отдельное требование.
Функция создаёт отдельный объект через spread. Оператор ?? сохраняет прежнее название или цену, если новое значение не передано или равно undefined. При этом цена 0 не теряется: она означает бесплатное мероприятие, а не отсутствие изменения.
Словарь с известными ключами
Для каждого формата мероприятия нужна русская подпись. Record позволяет проверить, что мы не забыли ни один вариант:
type EventFormat = "online" | "offline";
const formatLabels: Record<EventFormat, string> = {
online: "Онлайн",
offline: "Очно",
};
function getFormatLabel(format: EventFormat): string {
return formatLabels[format];
}Record<EventFormat, string> означает «у каждого ключа из EventFormat должно быть строковое значение». Удалите offline или добавьте в EventFormat новый вариант – редактор покажет незаполненную подпись.
Это удобный случай для Record: ключи заранее известны. Словарь Record<string, string> не обещает, что любой придуманный ключ действительно присутствует; при нашем noUncheckedIndexedAccess чтение неизвестного ключа может вернуть undefined.
Дополнительно: связанные значения в кортеже
Обычный массив подходит для любого количества однотипных элементов. Иногда позиции имеют разный смысл: например, координата состоит из двух чисел, где первое – горизонтальное положение, второе – вертикальное:
type Point = [x: number, y: number];
const position: Point = [120, 80];
const [x, y] = position;
console.log(x, y); // 120, 80Такой тип называют кортежем. Здесь он задаёт два элемента с определённым порядком. Для данных с большим количеством полей обычно понятнее объект с именами свойств.
Дополнительно: один алгоритм для разных типов
Функция, которая берёт первый элемент, не должна зависеть от того, что находится в массиве: числа, строки или мероприятия. При этом хочется сохранить точный тип результата:
function getFirst<Item>(items: readonly Item[]): Item | undefined {
return items[0];
}
const firstName = getFirst(["Кинопоказ", "Лекция"]);
const firstPrice = getFirst([300, 0]);
console.log(firstName?.toUpperCase()); // "КИНОПОКАЗ"
console.log(firstPrice); // 300
console.log(getFirst([])); // undefinedItem – параметр типа. Для первого вызова TypeScript подставляет string, для второго – number. Результат сохраняет эту связь с массивом. Возможность undefined нужна, потому что массив может быть пустым. ?. вызывает метод только при наличии значения.
Такие функции называют обобщёнными, или generic-функциями. Item не создаёт переменную при выполнении; это имя для типа. В документации часто встречается короткое имя T, оно выполняет ту же роль.
Вы уже видели похожую запись у Pick и Record: в угловых скобках им передают типы. В Promise<string> тип в скобках описывает значение успешного результата. Например:
async function getMessage(): Promise<string> {
return "Афиша готова";
}async-функция возвращает Promise, даже если внутри написан обычный return "...". Ошибку ожидания по-прежнему обрабатывают через try/catch; тип Promise<string> не обещает успешное завершение.
keyof: имя свойства тоже можно ограничить
Вместо произвольной строки можно принимать только ключи существующей модели:
type EventField = keyof EventItem;
const field: EventField = "price";
// const wrongField: EventField = "prise"; // Ошибка: такого свойства нет.keyof EventItem получает объединение имён его свойств. Это пригодится в таблице или настройках отображения, где пользователь выбирает поле. Собственные сложные обобщения не нужны для каждой функции: начните с готовых инструментов и вводите новый параметр типа, когда действительно требуется сохранить связь между входом и результатом.
Документация: типы объектов и readonly, Pick, Omit, Partial и Record, обобщённые функции.