Partial, Required, Readonly
Partial<T>,Required<T>иReadonly<T>— утилитарные типы TypeScript, которые трансформируют все свойства объектного типа: делают их опциональными, обязательными или только для чтения соответственно.
Зачем нужно
Эти утилиты позволяют создавать варианты типа без дублирования определений. Partial используется для update/patch операций, Required — для внутренних функций, которым гарантировано полное заполнение, Readonly — для неизменяемых данных и защиты от мутаций.
Где используется
Partial— DTO для PATCH-запросов, builder-паттерн, объект с опциямиRequired— после валидации, когда известно что поле заполненоReadonly— иммутабельные объекты конфигурации, frozen state в Redux
Основной контент
Partial<T>
Делает все свойства опциональными (T | undefined).
interface User {
id: string;
name: string;
email: string;
age: number;
}
type UserUpdate = Partial<User>;
// { id?: string; name?: string; email?: string; age?: number }
function updateUser(id: string, changes: Partial<User>): void {
// changes может содержать любое подмножество полей User
console.log(id, changes);
}
updateUser("u-1", { name: "Alice" }); // OK — только name
updateUser("u-1", { email: "a@b.com", age: 25 }); // OK
updateUser("u-1", {}); // OK — пустой объект
// Реализация:
// type Partial<T> = { [K in keyof T]?: T[K] };
Required<T>
Делает все свойства обязательными (убирает ?).
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
// После заполнения defaults — все поля есть
type FullConfig = Required<Config>;
// { host: string; port: number; debug: boolean }
function startServer(config: FullConfig): void {
console.log(`${config.host}:${config.port}`);
}
// Реализация:
// type Required<T> = { [K in keyof T]-?: T[K] };
Readonly<T>
Делает все свойства только для чтения.
interface Point {
x: number;
y: number;
}
const origin: Readonly<Point> = { x: 0, y: 0 };
// origin.x = 1; // Error — Cannot assign to 'x' because it is a read-only property
// Реализация:
// type Readonly<T> = { readonly [K in keyof T]: T[K] };
Комбинирование утилит
interface User {
id?: string;
name: string;
email?: string;
}
// Полный и неизменяемый
type ImmutableUser = Readonly<Required<User>>;
// Частично обновляемый но ID не меняется
type UserPatch = Partial<Omit<User, "id">>;
Практический пример: CRUD-сервис
interface Product {
id: string;
name: string;
price: number;
description: string;
}
// Для создания — без id (он генерируется)
type CreateProductDto = Omit<Product, "id">;
// Для обновления — только изменяемые поля, все опциональные
type UpdateProductDto = Partial<Omit<Product, "id">>;
// Для внутреннего хранения — неизменяемый
type StoredProduct = Readonly<Product>;
function createProduct(dto: CreateProductDto): StoredProduct {
return Object.freeze({ ...dto, id: crypto.randomUUID });
}
function patchProduct(id: string, dto: UpdateProductDto): void {
// dto содержит только изменённые поля
}
DeepReadonly — глубокая версия
// Стандартный Readonly не рекурсивен
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type ImmutableConfig = DeepReadonly<{
db: { host: string; port: number };
cache: { ttl: number };
}>;
// Все вложенные поля тоже readonly
Частые ошибки
Readonlyне защищает от мутации вложенных объектов — только поверхностная защита; для вложенных нуженDeepReadonlyилиObject.freeze.Partialделает все поля опциональными, включая обязательные — если нужно сделать опциональными только некоторые, используйтеPartial<Pick<T, K>>.- Путать
Requiredс NonNullable —Requiredубирает?, но неnull; полеname: string | nullостанетсяstring | null. - Использовать
ReadonlyвместоObject.freeze—Readonlyтолько на уровне типов; в рантайме объект можно мутировать.