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

Partial, Required и Readonly

Вернёмся к генератору анкет. Иногда нужна не сама анкета, а её черновик: те же поля, но любое можно пока оставить пустым. Так бывает, когда пользователь меняет в профиле одно поле, например только возраст. Программа получает не всю анкету, а только то, что изменилось.

Попробуем описать такое изменение типом User:

TypeScript
interface User {
    name: string;
    age: number;
    city: string;
}

function updateUser(user: User, changes: User): User {
    return { ...user, ...changes };
}

const ann: User = { name: "Ann", age: 30, city: "Omsk" };
updateUser(ann, { age: 31 }); // ✗
TEXT
error TS2739: Type '{ age: number; }' is missing the following properties from type 'User': name, city

tsc прав: в User все свойства обязательные, а в изменении есть только age. Можно написать второй интерфейс, где у каждого свойства стоит ?. Но тогда каждое поле опять записано дважды.

Утилитный тип Partial

Утилитный тип — обобщённый тип из стандартной библиотеки TypeScript, который строит новый тип из старого. Импортировать его не нужно: он доступен в любом файле. Partial<User> делает все свойства User необязательными — получается тот самый черновик:

TypeScript
interface User {
    name: string;
    age: number;
    city: string;
}

function updateUser(user: User, changes: Partial<User>): User {
    return { ...user, ...changes };
}

const ann: User = { name: "Ann", age: 30, city: "Omsk" };
const older = updateUser(ann, { age: 31 });
console.log(older.name, older.age); // Ann 31

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

  1. Partial<User> — тот же тип, что { name?: string; age?: number; city?: string }.
  2. Объект { age: 31 } подходит: остальные свойства можно не указывать.
  3. { ...user, ...changes } собирает нового пользователя: сначала все старые свойства, поверх них — изменённые.
  4. Программа печатает Ann 31: имя осталось прежним, а возраст обновился.

Остальные проверки никуда не делись. Свойства email в Partial<User> нет, поэтому { email: "ann@mail.com" } tsc не пропустит. Не пройдёт и { age: "31" }: в age по-прежнему лежит number.

Здесь аналогия с черновиком перестаёт работать. Заполненный до конца черновик можно сдать как анкету, а значение типа Partial<User> не подходит к User, даже если в нём есть все поля. tsc видит тип, а в этом типе любое свойство может отсутствовать.

Как устроен Partial

Partial — не особый механизм компилятора. Он собран из keyof и доступа по ключу, которые вы уже знаете, и такой же тип можно написать самому:

TypeScript
interface User {
    name: string;
    age: number;
    city: string;
}

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

const change: Draft<User> = { city: "Tomsk" };
console.log(change.city); // Tomsk

Запись { [K in keyof T]?: T[K] } читается так:

  • [K in keyof T] — «для каждого имени K из keyof T»: свойство повторяется для каждого имени;
  • ? после скобок делает каждое такое свойство необязательным;
  • T[K] оставляет свойству тот же тип, что и в T.

В стандартной библиотеке Partial записан почти так же, только параметр называется P. Required и Readonly устроены по тому же образцу: они тоже проходят по keyof T и меняют у каждого свойства одну пометку.

Required и Readonly

Required

Required<T> делает обратное: снимает ? со всех свойств. Он пригодится для настроек со значениями по умолчанию. Пусть настройки описаны как interface Settings { theme?: string; fontSize?: number }: пользователь может задать любые из них. А у значений по умолчанию должно быть заполнено всё:

  • запись const defaults: Required<Settings> = { theme: "dark", fontSize: 14 }; компилируется;
  • если забыть fontSize, tsc назовёт пропущенное свойство.
TEXT
error TS2741: Property 'fontSize' is missing in type '{ theme: string; }' but required in type 'Required<Settings>'.

Readonly

Readonly<T> помечает все свойства readonly, как будто вы написали это слово у каждого. После const ann: Readonly<User> = { name: "Ann", age: 30, city: "Omsk" }; запись ann.age = 31 не скомпилируется:

TEXT
error TS2540: Cannot assign to 'age' because it is a read-only property.

Как и сам readonly, эту защиту проверяет только tsc. Readonly<User> не замораживает объект: из JavaScript он исчезает вместе с остальными типами, и при запуске свойство можно изменить.

Что дают Partial, Required и Readonly, часто спрашивают на собеседовании. Ответ: каждый проходит по всем свойствам T и ставит или снимает у них одну пометку — ? или readonly.

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

Ждали, что Partial сделает необязательными и свойства вложенного объекта:

TypeScript
interface Order {
    id: number;
    address: { city: string; street: string };
}

const fix: Partial<Order> = { address: { city: "Omsk" } }; // ✗
TEXT
error TS2741: Property 'street' is missing in type '{ city: string; }' but required in type '{ city: string; street: string; }'.

Все три утилиты меняют только свойства верхнего уровня. В Partial<Order> можно не указывать address, но если он указан, это полный адрес со всеми полями. Так же Readonly<Order> запрещает заменить address целиком, но разрешает изменить address.city. Чтобы поменять город, передайте адрес целиком: { address: { city: "Omsk", street: "Lenina" } }.

Главное

  • Partial<T> делает все свойства T необязательными, Required<T> — обязательными, Readonly<T> — только для чтения.
  • Все три проходят по keyof T, как { [K in keyof T]?: T[K] } у Partial, и меняют у каждого свойства одну пометку.
  • Они меняют только верхний уровень, а свойства вложенного объекта остаются как были.
  • Это типы: tsc их проверяет и стирает, и Readonly не замораживает объект при запуске.