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 с NonNullableRequired убирает ?, но не null; поле name: string | null останется string | null.
  • Использовать Readonly вместо Object.freezeReadonly только на уровне типов; в рантайме объект можно мутировать.

Связанные темы

Ресурсы