Partial<T> это встроенный служебный тип, который делает каждое свойство T необязательным. Это естественный тип для обновления или патча: вызывающий код передаёт только изменившиеся поля.
Partial<User> это { id?: number; name?: string; email?: string }. Компилятор по-прежнему проверяет поля, которые вы передаёте: { nmae: "x" } или { name: 42 } это ошибка, и именно поэтому Partial лучше расплывчатого параметра object или any.
Как определён Partial
Partial это однострочный сопоставленный тип в стандартной библиотеке TypeScript:
type Partial<T> = {
[P in keyof T]?: T[P];
};
Для каждого ключа P из T он объявляет необязательное свойство того же типа. Поскольку он перебирает keyof T, он сохраняет readonly у свойств, где он был. Это обычный тип, поэтому он не влияет на выполнение: объект, переданный как changes, в любом случае один и тот же.
Чтение из Partial даёт T | undefined
Любое свойство Partial<T> может отсутствовать, поэтому его чтение даёт тип свойства плюс undefined. Компилятор заставляет обработать случай отсутствия:
{ ...defaults, ...opts } это обычный способ превратить Partial<Options> обратно в полный Options: сначала развернуть значения по умолчанию, а затем позволить переданным значениям их переопределить.
Ловушка явного undefined
Необязательное свойство может отсутствовать, но может и присутствовать со значением undefined. Развёртывание объекта копирует это undefined поверх настоящего значения, а тип результата этого не показывает:
Это кусается, когда патч собирается из формы или строки запроса, где пустые поля становятся undefined. Параметр компилятора exactOptionalPropertyTypes (не входит в strict) делает { name: undefined } ошибкой компиляции для name?: string, если не написать name?: string | undefined, и это предотвращает проблему в источнике.
Partial поверхностный
Partial делает необязательными только свойства верхнего уровня. Вложенный объект, если вы его передаёте, должен быть полным:
index.ts(8,3): error TS2741: Property 'tabSize' is missing in type '{ fontSize: number; }' but required in type '{ fontSize: number; tabSize: number; }'.
Свойство editor необязательное, но если оно есть, его тип { fontSize: number; tabSize: number } не изменился. Для объектов настроек, патчей API и тестовых данных часто нужны необязательные свойства на каждом уровне. Для этого нужен рекурсивный тип.
DeepPartial: рекурсивный Partial
Встроенной глубокой версии в TypeScript нет, но она пишется в несколько строк. Функции и массивы остаются как есть, потому что необязательные элементы массива разрешили бы [undefined]:
Тип рекурсивный, а слияние нет. applySettings сливает editor вручную, потому что развёртывание объекта тоже поверхностное. Обобщённая функция глубокого слияния есть в библиотеках вроде lodash (merge), и её типизация сложнее, чем у типа выше.
Required: противоположность Partial
Required<T> убирает ? у каждого свойства. Он определён с модификатором -?, который ещё и убирает undefined из типа каждого свойства:
Приём тот же, что с Partial, только наоборот: пользователи API передают расплывчатую конфигурацию, а код внутри работает с версией Required, где о каждом значении известно, что оно есть. У развёртывания та же дыра, что у функции обновления выше: вызывающий код, который явно передаёт port: undefined, перезаписывает значение по умолчанию на undefined, и компилятор это принимает, если не включён exactOptionalPropertyTypes. Required поверхностный так же, как Partial.
Необязательными или обязательными только некоторые свойства
Partial и Required применяются ко всем свойствам. Чтобы изменить только несколько, разделите тип через Pick и Omit и соберите его обратно:
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
interface Post {
id: number;
title: string;
body?: string;
}
type NewPost = PartialBy<Post, "id">; // id optional, title required, body optional
type Published = RequiredBy<Post, "body">; // body now required
| Тип | Эффект | Глубокий? |
|---|---|---|
Partial<T> | каждое свойство необязательное | нет |
Required<T> | каждое свойство обязательное, undefined убран | нет |
DeepPartial<T> (собственный) | необязательные на каждом уровне | да |
PartialBy<T, K> (собственный) | необязательны только ключи K | нет |
Readonly<T> | каждое свойство readonly | нет |
Часто задаваемые вопросы
Что делает Partial в TypeScript?
Partial<T> создаёт тип, в котором все свойства T помечены как необязательные. Для interface User { name: string; email: string } Partial<User> это { name?: string; email?: string }, поэтому {}, { name: "Ada" } и полный пользователь это допустимые значения.
Partial в TypeScript глубокий?
Нет, Partial влияет только на свойства верхнего уровня. Вложенный объект внутри Partial<T> по-прежнему должен быть полным. Для рекурсивной версии напишите тип DeepPartial<T>, который применяет себя к свойствам с объектным типом.
Что противоположно Partial в TypeScript?
Required<T>. Он убирает ? у каждого свойства и заодно убирает undefined из их типов, поэтому Required<{ port?: number }> это { port: number }. Он определён как сопоставленный тип с модификатором -?.
Как сделать необязательными только некоторые свойства?
Сочетайте Omit, Pick и Partial: type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>. PartialBy<User, "email"> оставляет все свойства как были, кроме email, которое становится необязательным.
Почему свойство стало undefined после слияния с Partial-обновлением?
Partial<T> разрешает свойству присутствовать со значением undefined, а развёртывание объекта его копирует: { ...user, ...{ name: undefined } } содержит name: undefined, хотя TypeScript типизирует результат как User. Отфильтруйте значения undefined перед слиянием или включите exactOptionalPropertyTypes, чтобы явное undefined отвергалось.