Перейти к содержимому
Теория · Операторы типов и утилиты · урок 7 из 8

Pick, Omit и Record

Из полной анкеты генератор умеет делать и короткие карточки. Для списка пользователей на сайте нужны только номер и имя, а для публичной страницы — всё, кроме почты. Можно описать каждую карточку своим интерфейсом, но тогда поля снова записаны в нескольких местах. Поменяете тип поля в User — и придётся искать все копии.

Утилитные типы Pick и Omit вырезают из готового типа нужную часть, а Record собирает объектный тип из списка ключей. В конце урока — ReturnType, который берёт тип результата у функции.

Pick и Omit

Оба принимают тип и имена его свойств, но делают противоположное:

TypeScript
interface User {
    id: number;
    name: string;
    email: string;
    age: number;
}

type Card = Pick<User, "id" | "name">;
type PublicUser = Omit<User, "email">;

const card: Card = { id: 1, name: "Ann" };
const shown: PublicUser = { id: 1, name: "Ann", age: 30 };
console.log(card.name, shown.age); // Ann 30

Pick

Pick<User, "id" | "name"> оставляет только перечисленные свойства: получается тип { id: number; name: string }. Имена передают объединением литералов, и TypeScript проверяет каждое. Назвать можно только настоящее свойство User, а на Pick<User, "phone"> tsc ответит ошибкой:

TEXT
error TS2344: Type '"phone"' does not satisfy the constraint 'keyof User'.

В определении Pick стоит знакомое ограничение K extends keyof T.

Omit

Omit<User, "email"> делает наоборот: убирает перечисленные свойства и оставляет все остальные — id, name и age. Его удобно брать, когда убрать нужно меньше, чем оставить.

Оба типа следят за исходным. Поменяйте тип name в User, и он поменяется и в Card, и в PublicUser. Чем Pick отличается от Omit, любят спрашивать на собеседовании.

Record

Record<K, V> строит объект по списку ключей: каждый ключ из K становится свойством со значением типа V. Так удобно описывать таблицы, например переводы, где для каждого языка нужна строка:

TypeScript
const greetings: Record<"ru" | "en", string> = {
    ru: "Привет",
    en: "Hello",
};
const stock: Record<string, number> = { apple: 5 };
console.log(greetings.en, stock["apple"]); // Hello 5

Давайте разберём пример по строкам:

  1. Record<"ru" | "en", string> — объект со свойствами ru и en, и в обоих строки.
  2. Оба свойства обязательные: tsc проверит, что ни один язык не забыт.
  3. Record<string, number> — ключ любая строка, значение число. Это та же индексная сигнатура { [key: string]: number }, только записанная короче.
  4. Программа печатает Hello 5.

Если забыть язык, например написать только { ru: "Привет" }, tsc назовёт пропущенный ключ. В сообщении TypeScript 7 печатает ключи отсортированными, поэтому "en" стоит первым:

TEXT
error TS2741: Property 'en' is missing in type '{ ru: string; }' but required in type 'Record<"en" | "ru", string>'.

ReturnType: тип результата

Ещё одна утилита берёт тип у функции. ReturnType<F> — тип того, что возвращает функция типа F. Утилите нужен тип функции, а не сама функция, поэтому перед именем стоит typeof:

TypeScript
function parseUser(line: string) {
    const parts = line.split(",");
    return { name: parts[0], age: Number(parts[1]) };
}

type ParsedUser = ReturnType<typeof parseUser>;

const p: ParsedUser = parseUser("Ann,30");
console.log(p.name, p.age); // Ann 30

Разберём, откуда взялся тип ParsedUser:

  1. Для результата parseUser TypeScript выводит тип { name: string; age: number; }.
  2. typeof parseUser — тип самой функции: (line: string) => { name: string; age: number; }.
  3. ReturnType достаёт из этого типа тип результата. Описывать ParsedUser руками не пришлось.

Если забыть typeof, tsc подскажет, чего не хватает:

TEXT
error TS2749: 'parseUser' refers to a value, but is being used as a type here. Did you mean 'typeof parseUser'?

Частая ошибка

В Omit опечатались в имени свойства:

TypeScript
interface User {
    id: number;
    name: string;
    email: string;
}

type Preview = Omit<User, "emial">;

const p: Preview = { id: 1, name: "Ann" }; // ✗
TEXT
error TS2741: Property 'email' is missing in type '{ id: number; name: string; }' but required in type 'Preview'.

На строке с Omit tsc молчит. В отличие от Pick, Omit не проверяет имена: его ограничение пропускает любую строку, число или symbol, а не только keyof T. Свойства emial в User нет, убирать оказалось нечего, и email остался в Preview. Ошибка всплыла дальше, у объекта без почты. Если tsc требует свойство, которое вы убрали через Omit, проверьте имя в самом Omit.

Главное

  • Pick<User, "id" | "name"> оставляет только названные свойства, Omit<User, "email"> — все, кроме названных.
  • Record<"ru" | "en", string> — объект, где каждый ключ из объединения обязателен и хранит string.
  • ReturnType<typeof f> — тип результата функции f; typeof нужен, потому что утилите нужен тип, а не значение.
  • Pick проверяет имена свойств, а Omit — нет: опечатка в Omit проходит молча.