Utility Types

Встроенные generic-типы TypeScript для трансформации существующих типов: Partial, Required, Readonly, Pick, Omit, Record и другие.

Зачем нужно

  • Не писать дублирующиеся типы вручную
  • Трансформировать существующие типы: сделать все поля опциональными, выбрать подмножество, сделать readonly
  • Стандартные утилиты понятны всем TypeScript-разработчикам
  • Уменьшение boilerplate при работе с API, формами, состоянием

Где используется

  • Partial<T> — формы, обновления (PATCH-запросы)
  • Pick<T, K> / Omit<T, K> — DTO, подмножество полей
  • Record<K, V> — словари, маппинги
  • ReturnType<T> — извлечение типа из функции
  • Awaited<T> — распаковка Promise

Предпосылки

Partial<T> — все свойства опциональные

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Все поля становятся опциональными
type PartialUser = Partial<User>;
// {
//   id?: number;
//   name?: string;
//   email?: string;
//   age?: number;
// }

// Применение: обновление (PATCH)
function updateUser(id: number, updates: Partial<User>): User {
  const user = getUser(id);
  return { ...user, ...updates };
}

updateUser(1, { name: "Bob" });         // OK
updateUser(1, { age: 25, email: "b@b" }); // OK
updateUser(1, {});                       // OK

// Реализация:
// type Partial<T> = { [K in keyof T]?: T[K] };

Required<T> — все свойства обязательные

interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}

// Все поля обязательные
type FullConfig = Required<Config>;
// {
//   host: string;
//   port: number;
//   debug: boolean;
// }

// Применение: валидированная конфигурация
function startServer(config: Required<Config>): void {
  console.log(`Starting on ${config.host}:${config.port}`);
}

// Реализация:
// type Required<T> = { [K in keyof T]-?: T[K] };

Readonly<T> — все свойства только для чтения

interface User {
  id: number;
  name: string;
}

type ReadonlyUser = Readonly<User>;
// {
//   readonly id: number;
//   readonly name: string;
// }

const user: ReadonlyUser = { id: 1, name: "Alice" };
user.name = "Bob"; // Ошибка! Cannot assign to 'name'

// Применение: иммутабельное состояние
function freeze<T>(obj: T): Readonly<T> {
  return Object.freeze(obj);
}

// Реализация:
// type Readonly<T> = { readonly [K in keyof T]: T[K] };

Pick<T, K> — выбрать свойства

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  role: string;
}

// Выбираем только нужные свойства
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }

type UserContact = Pick<User, "name" | "email">;
// { name: string; email: string }

// Применение: API response DTO
function getUserPreview(id: number): Pick<User, "id" | "name" | "role"> {
  const user = getUser(id);
  return { id: user.id, name: user.name, role: user.role };
}

// Реализация:
// type Pick<T, K extends keyof T> = { [P in K]: T[P] };

Omit<T, K> — исключить свойства

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// Убираем чувствительные поля
type SafeUser = Omit<User, "password">;
// { id: number; name: string; email: string; createdAt: Date }

// Данные для создания (без автогенерируемых полей)
type CreateUserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; password: string }

// Применение: API DTO
function createUser(input: Omit<User, "id" | "createdAt">): User {
  return {
    ...input,
    id: generateId,
    createdAt: new Date,
  };
}

// Реализация:
// type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Record<K, V> — объект с ключами K и значениями V

// Словарь
type UserMap = Record<string, User>;
const users: UserMap = {
  alice: { id: 1, name: "Alice", email: "a@b.com" },
  bob: { id: 2, name: "Bob", email: "b@b.com" },
};

// Enum-ключи
type Role = "admin" | "user" | "guest";
type RolePermissions = Record<Role, string>;

const permissions: RolePermissions = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"],
};

// Маппинг статусов
type StatusColor = Record<"success" | "error" | "warning", string>;
const colors: StatusColor = {
  success: "#00ff00",
  error: "#ff0000",
  warning: "#ffff00",
};

// Реализация:
// type Record<K extends keyof any, T> = { [P in K]: T };

Extract<T, U> — извлечь типы из union

type AllTypes = string | number | boolean | null | undefined;

// Извлечь только те, что extends string | number
type Primitives = Extract<AllTypes, string | number>;
// string | number

// Из событий
type Event = "click" | "scroll" | "mousemove" | "keydown" | "keyup";
type MouseEvent = Extract<Event, "click" | "scroll" | "mousemove">;
// "click" | "scroll" | "mousemove"

// Реализация:
// type Extract<T, U> = T extends U ? T : never;

Exclude<T, U> — исключить типы из union

type AllTypes = string | number | boolean | null | undefined;

// Убрать null и undefined
type NonNullTypes = Exclude<AllTypes, null | undefined>;
// string | number | boolean

// Из событий
type Event = "click" | "scroll" | "mousemove" | "keydown" | "keyup";
type NonMouseEvent = Exclude<Event, "click" | "scroll" | "mousemove">;
// "keydown" | "keyup"

// Реализация:
// type Exclude<T, U> = T extends U ? never : T;

NonNullable<T> — убрать null и undefined

type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string

// Применение
function process(value: string | null | undefined): string {
  const safe: NonNullable<typeof value> = value!; // assertion
  return safe.toUpperCase();
}

// Реализация:
// type NonNullable<T> = T & {};
// (эквивалентно Exclude<T, null | undefined>)

ReturnType<T> — тип возвращаемого значения функции

function getUser() {
  return { id: 1, name: "Alice", email: "alice@example.com" };
}

type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string }

// Для async функций
async function fetchUsers() {
  return [{ id: 1, name: "Alice" }];
}

type FetchResult = ReturnType<typeof fetchUsers>;
// Promise<{ id: number; name: string }>

// Для получения распакованного типа
type Users = Awaited<ReturnType<typeof fetchUsers>>;
// { id: number; name: string }

// Реализация:
// type ReturnType<T extends (...args: any) => any> =
//   T extends (...args: any) => infer R ? R : any;

Parameters<T> — типы параметров функции

function createUser(name: string, age: number, isAdmin: boolean): void {}

type Params = Parameters<typeof createUser>;
// [string, number, boolean]

// Извлечение конкретного параметра
type FirstParam = Parameters<typeof createUser>[0]; // string
type SecondParam = Parameters<typeof createUser>[1]; // number

// Применение: обёртка функции
function withLogging<T extends (...args: any) => any>(
  fn: T
): (...args: Parameters<T>) => ReturnType<T> {
  return (...args) => {
    console.log("Calling with:", args);
    return fn(...args);
  };
}

// Реализация:
// type Parameters<T extends (...args: any) => any> =
//   T extends (...args: infer P) => any ? P : never;

Awaited<T> — распаковка Promise

type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number (рекурсивно)
type C = Awaited<string | Promise<number>>; // string | number

// Применение с async функциями
async function fetchData: Promise<{ users: User }> {
  // ...
}

type Data = Awaited<ReturnType<typeof fetchData>>;
// { users: User }

Комбинирование utility types

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  role: "admin" | "user";
  createdAt: Date;
  updatedAt: Date;
}

// CREATE: без id и timestamps
type CreateUser = Omit<User, "id" | "createdAt" | "updatedAt">;

// UPDATE: все поля кроме id опциональные
type UpdateUser = Partial<Omit<User, "id">> & Pick<User, "id">;

// PUBLIC: без пароля, readonly
type PublicUser = Readonly<Omit<User, "password">>;

// FILTER: только для поиска
type UserFilter = Partial<Pick<User, "name" | "email" | "role">>;

// LIST RESPONSE
type UserListResponse = {
  data: PublicUser;
  total: number;
  page: number;
};

Частые ошибки

  1. Omit с несуществующим ключом — не даст ошибку, просто ничего не исключит
type Result = Omit<User, "nonExistent">; // Нет ошибки! Возвращает весь User
  1. Partial делает неглубокий partial — вложенные объекты остаются обязательными
interface Config {
  server: { host: string; port: number }; // Всё ещё Required
}
type P = Partial<Config>;
// { server?: { host: string; port: number } }
// server опционален, но если задан — host и port обязательны
  1. Readonly — поверхностный — вложенные объекты можно мутировать
  2. Забыть typeof при ReturnType — нужен тип функции, а не сама функция
function getUser() { return { name: "Alice" }; }
type A = ReturnType<getUser>; // Ошибка!
type B = ReturnType<typeof getUser>; // OK

Практика

  1. Создайте CRUD-типы из одного интерфейса: CreateDTO, UpdateDTO, ResponseDTO
  2. Реализуйте DeepPartial<T> вручную (рекурсивный Partial)
  3. Используйте Record для создания словаря с enum-ключами
  4. Извлеките тип возвращаемого значения async-функции через Awaited<ReturnType<>>
  5. Комбинируйте Pick + Partial + Required для формы с обязательными и опциональными полями

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

Ресурсы