Partial, Required и Readonly
Вернёмся к генератору анкет. Иногда нужна не сама анкета, а её черновик: те же поля, но любое можно пока оставить пустым. Так бывает, когда пользователь меняет в профиле одно поле, например только возраст. Программа получает не всю анкету, а только то, что изменилось.
Попробуем описать такое изменение типом User:
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 }); // ✗error TS2739: Type '{ age: number; }' is missing the following properties from type 'User': name, citytsc прав: в User все свойства обязательные, а в изменении есть только age. Можно написать второй интерфейс, где у каждого свойства стоит ?. Но тогда каждое поле опять записано дважды.
Утилитный тип Partial
Утилитный тип — обобщённый тип из стандартной библиотеки TypeScript, который строит новый тип из старого. Импортировать его не нужно: он доступен в любом файле. Partial<User> делает все свойства User необязательными — получается тот самый черновик:
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Давайте разберём пример по строкам:
Partial<User>— тот же тип, что{ name?: string; age?: number; city?: string }.- Объект
{ age: 31 }подходит: остальные свойства можно не указывать. { ...user, ...changes }собирает нового пользователя: сначала все старые свойства, поверх них — изменённые.- Программа печатает
Ann 31: имя осталось прежним, а возраст обновился.
Остальные проверки никуда не делись. Свойства email в Partial<User> нет, поэтому { email: "ann@mail.com" } tsc не пропустит. Не пройдёт и { age: "31" }: в age по-прежнему лежит number.
Здесь аналогия с черновиком перестаёт работать. Заполненный до конца черновик можно сдать как анкету, а значение типа Partial<User> не подходит к User, даже если в нём есть все поля. tsc видит тип, а в этом типе любое свойство может отсутствовать.
Как устроен Partial
Partial — не особый механизм компилятора. Он собран из keyof и доступа по ключу, которые вы уже знаете, и такой же тип можно написать самому:
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 назовёт пропущенное свойство.
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 не скомпилируется:
error TS2540: Cannot assign to 'age' because it is a read-only property.Как и сам readonly, эту защиту проверяет только tsc. Readonly<User> не замораживает объект: из JavaScript он исчезает вместе с остальными типами, и при запуске свойство можно изменить.
Что дают Partial, Required и Readonly, часто спрашивают на собеседовании. Ответ: каждый проходит по всем свойствам T и ставит или снимает у них одну пометку — ? или readonly.
Частая ошибка
Ждали, что Partial сделает необязательными и свойства вложенного объекта:
interface Order {
id: number;
address: { city: string; street: string };
}
const fix: Partial<Order> = { address: { city: "Omsk" } }; // ✗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не замораживает объект при запуске.