Полезные типы TypeScript — те, которые стоит запоминать (и остальные)

Обновлено

Частичный, Выбор, Пропуск, Запись, Тип возвращаемого значения, Параметры, Ожидаемый — восемь полезных типов с примерами до и после и честным мнением о том, какие из них вы будете использовать каждый день.

TypeScript Utility Types — The Ones Worth Memorizing (and the Rest) 1
Реклама · УДАЛИТЬ?

Вы, вероятно, уже читали страницу документации по TypeScript, на которой перечислены более 20 полезных типов в алфавитном порядке, каждый из которых объясняется в одном предложении, и это оставило вас с вопросом, какие из них стоит действительно запомнить, а какие предназначены исключительно для разработчиков фреймворков. Вот более честный вариант: восемь типов, с конкретными примерами до и после, и прямой ответ о том, какие из них используются ежедневно, а какие — редко.

Если вы начинаете с чистого JSON и ещё не написали свои интерфейсы, то конвертер JSON в TypeScript поможет вам получить базовый интерфейс за несколько секунд — затем вернитесь сюда, чтобы добавить эти полезные типы.

Partial — ежедневное использование. Изучайте его первым.

Сделает все свойства в T необязательными. Классический случай — обновления в стиле PATCH, когда вы отправляете только поля, которые изменились.

// Before: manually duplicating the interface with optional versions of everything
interface UserUpdate {
  name?: string;
  email?: string;
  age?: number;
}

// After: Partial derives it from the source of truth
interface User {
  name: string;
  email: string;
  age: number;
}

function updateUser(id: string, updates: Partial<User>) {
  // updates.name is string | undefined — TypeScript knows
}

Проблема с версией «до»: если вы добавляете поле в User, вам нужно помнить, чтобы добавить его и в UserUpdate . С помощью Partial<User>добавление поля в исходный интерфейс автоматически делает его доступным для обновлений. Типы остаются синхронизированными.

Required — ежемесячное использование, но помогает в одном конкретном сценарии.

Противоположность Partial — удаляет все ? из типа. Наиболее полезно после валидации: вы получаете объект с нестрогим типом конфигурации, проверяете, что все поля присутствуют, и хотите, чтобы TypeScript перестал предлагать, что все поля могут быть неопределёнными.

// Before: after validating, you still fight undefined checks everywhere
interface Config {
  apiUrl?: string;
  timeout?: number;
  retries?: number;
}

function initApp(config: Config) {
  config.apiUrl?.trim(); // need optional chaining even after checking
}

// After: Required signals "this has been validated, stop warning me"
function validateAndInit(raw: Config): void {
  if (!raw.apiUrl) throw new Error("apiUrl required");
  if (!raw.timeout) throw new Error("timeout required");
  if (!raw.retries) throw new Error("retries required");

  const config = raw as Required<Config>;
  config.apiUrl.trim(); // no optional chaining needed
}

Честное замечание: Required<T> не выполняет валидацию на уровне выполнения — вам всё равно нужно писать проверки. Оно лишь меняет то, что TypeScript считает в дальнейшем. Используйте его для передачи сигнала «это уже было валидировано» на уровне типов.

Pick — ежедневное использование. Выбираете, что хотите.

Создаёт новый тип, содержащий только свойства, которые вы указываете из T. Полезно, когда нужно получить подмножество более крупного интерфейса — формы публичного API, представления, сериализованные ответы.

// Before: manually maintaining a separate interface that drifts from User
interface UserPublicProfile {
  name: string;
  bio: string;
  avatarUrl: string;
}

// After: Pick stays in sync with the source
interface User {
  id: string;
  name: string;
  email: string;       // private — shouldn't go to clients
  passwordHash: string;  // definitely private
  bio: string;
  avatarUrl: string;
}

type UserPublicProfile = Pick<User, "name" | "bio" | "avatarUrl">;

Pick — правильный инструмент, когда список свойств, которые вы хотите короче, чем список, который вам нужно exclude. Когда ситуация наоборот, обращайтесь к Omit.

Omit — ежедневное использование. Указывайте, что не хотите.

Дополнение к Pick — создаёт тип с всеми свойствами из T кроме тех, которые вы указываете. Когда у вас есть большой интерфейс и только несколько полей, которые нужно исключить, Omit делает намерение более понятным, чем перечисление всех нужных полей.

// Before: manually listing every field except the sensitive ones
// Easy to miss a new field added to User later
interface UserForClient {
  id: string;
  name: string;
  email: string;
  bio: string;
  avatarUrl: string;
  // passwordHash intentionally omitted — but a future dev might not know that
}

// After: Omit states the exclusion explicitly
type UserForClient = Omit<User, "passwordHash">;
// If User gains a new "address" field, UserForClient gets it automatically

Одно из того, что ни Pick, ни Omit не делают: изменение типа отдельных свойств. Если вы хотите выбрать поля и и изменить их типы, вам нужно обратиться к отображающему типу, который — это совершенно другой инструмент.

Record — ежедневное использование. Типизированный словарь.

Создаёт тип объекта, где все ключи имеют тип K и все значения имеют тип V. Реальный выигрыш появляется, когда K является объединённым типом — TypeScript выдаст ошибку, если вы упустите ключ. Общие индексные сигнатуры безусловно принимают неполные объекты; Record с объединённым ключом не допускает этого.

// Before: index signature with no exhaustiveness checking
const rolePermissions: { [key: string]: string[] } = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  // forgot "viewer" — TypeScript doesn't notice
};

// After: Record with a union key enforces completeness
type Role = "admin" | "editor" | "viewer";

const rolePermissions: Record<Role, string[]> = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"],
  // TypeScript error if any Role is missing
  // TypeScript error if you try to add an unknown key
};

Проверка исчерпывающего покрытия — настоящий выигрыш. Это важно, когда ключи представляют что-то значимое, например роли, типы событий, HTTP-методы или коды состояний — любые случаи, где отсутствие значения — это ошибка, а не просто пропуск.

ReturnType — ежедневное использование, если вы используете функции сторонних библиотек.

Извлекает тип возвращаемого значения функции. Наиболее полезно при использовании функции из библиотеки и желании типизировать переменную, содержащую её результат — без необходимости импортировать тип возвращаемого значения отдельно (что библиотеки не всегда делают).

// Before: manually annotating the return type — it can drift
function getCurrentUser() {
  return { id: "u1", name: "Alice", roles: ["admin"] as const };
}

let cachedUser: { id: string; name: string; roles: readonly string[] };
// If getCurrentUser's return changes, this annotation doesn't update automatically

// After: ReturnType derives it and keeps in sync
type CurrentUser = ReturnType<typeof getCurrentUser>;
let cachedUser: CurrentUser;

// Especially useful with unexported library types:
import { useQuery } from "@tanstack/react-query";
type QueryResult = ReturnType<typeof useQuery>;
// No need to figure out what QueryObserverResult<...> looks like or import it

Parameters — редкое использование. В основном для обёрток.

Извлекает типы параметров функции в виде кортежа. Честный взгляд: вы не будете использовать это ежедневно, если не пишете много обёрток или высокопроизводительных утилит. Но когда вам это нужно, нет чистого альтернативы.

// Before: manually duplicating parameter types in the wrapper
function createEvent(
  type: string,
  payload: Record<string, unknown>,
  timestamp: number
) { /* ... */ }

// Every time createEvent changes, this wrapper needs a manual update:
function loggedCreateEvent(
  type: string,
  payload: Record<string, unknown>,
  timestamp: number
) {
  console.log("Creating event:", type);
  return createEvent(type, payload, timestamp);
}

// After: Parameters keeps the wrapper in sync automatically
function loggedCreateEvent(...args: Parameters<typeof createEvent>) {
  console.log("Creating event:", args[0]);
  return createEvent(...args);
}

Паттерн расширения и остатка (...args: Parameters<F>) — основной идиоматический подход. Он также встречается в тестовых моках: jest.fn<ReturnType<F>, Parameters<F>>() позволяет создать полностью типизированный мок без необходимости импортировать сигнатуру функции отдельно.

Awaited — используйте, когда нужно развернуть типы Promise без вызова функции.

Добавлено в TypeScript 4.5 (выпущено в ноябре 2021 года). Рекурсивно разворачивает Promise<T> для получения T. До его появления, получение развернутого типа из асинхронной функции означало написание условного типа: ReturnType<F> extends Promise<infer U> ? U : never — что нарушает работу при двойном обёртывании Promise и трудно читается в сравнениях.

// Before: conditional type to unwrap — breaks on nested Promises
async function fetchUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  return res.json() as { id: string; name: string; email: string };
}

type FetchedUser =
  ReturnType<typeof fetchUser> extends Promise<infer U> ? U : never;
// Works for Promise<T> but not Promise<Promise<T>>

// After: Awaited handles both cases cleanly
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;

// Awaited recursively unwraps:
type A = Awaited<Promise<string>>;            // string
type B = Awaited<Promise<Promise<string>>>;  // string
type C = Awaited<string>;                      // string (no-op on non-Promise)

Комбинация Awaited<ReturnType<typeof fn>> стоит запомнить как единое целое — она охватывает 90% случаев извлечения типов асинхронных функций в приложениях.

Краткое руководство

Полезный типЧто делаетЧастота использованияОсновное назначение
Partial<T>Все свойства становятся необязательнымиЕженедельноОбновления в стиле PATCH
Required<T>Все необязательные свойства становятся обязательнымиЕжемесячноСигнал «это завершено» после валидации
Pick<T, K>Сохраняются только названные свойстваЕженедельноФормы публичного API, представления
Omit<T, K>Удаляются названные свойстваЕженедельноУдаление чувствительных или ненужных полей
Record<K, V>Объект с ключами типа K и значениями типа VЕженедельноТипизированные словари с проверкой исчерпывающего покрытия
ReturnType<F>Извлекает тип возвращаемого значения функцииЕжедневное (особенно при работе с библиотеками)Типизирует переменную, содержащую результат из неимпортированного или неэкспортированного возвращаемого типа
Parameters<F>Извлекает типы параметров функции в виде кортежаРедкое использованиеОбёртки функций, типизированные моки
Awaited<T>Рекурсивно разворачивает типы PromiseРедкое использованиеПолучение развернутого типа из асинхронной функции

Те, что не указаны в этом списке

TypeScript также предоставляет Readonly<T>, NonNullable<T>, Extract<T,U>, Exclude<T,U>, InstanceType<C>, ConstructorParameters<C>и несколько типов шаблонных строк. Они реальны и иногда полезны, но встречаются в коде библиотек, универсальных утилит и метапрограммирования на уровне типов — не в типичной логике приложений. Если вы запомнили восемь типов, перечисленных выше, изучайте остальные, когда вам действительно понадобятся.

Основная идея одинакова для всех из них: описывает преобразование существующего типа, а не его дублирование. Конкретные названия полезных типов — это просто словарь для выражения этой цели на уровне TypeScript.

Хотите убрать рекламу? Откажитесь от рекламы сегодня

Установите наши расширения

Добавьте инструменты ввода-вывода в свой любимый браузер для мгновенного доступа и более быстрого поиска

в Расширение Chrome в Расширение края в Расширение Firefox в Расширение Opera

Табло результатов прибыло!

Табло результатов — это интересный способ следить за вашими играми, все данные хранятся в вашем браузере. Скоро появятся новые функции!

Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?
Реклама · УДАЛИТЬ?

новости с техническими моментами

Примите участие

Помогите нам продолжать предоставлять ценные бесплатные инструменты

Купи мне кофе
Реклама · УДАЛИТЬ?