Back

5 утилитарных типов TypeScript, которые вы должны знать

5 утилитарных типов TypeScript, которые вы должны знать

Система типов TypeScript становится по-настоящему мощной, когда вы открываете для себя утилитарные типы — встроенные помощники, которые преобразуют существующие типы без их переписывания. Если вы когда-либо дублировали интерфейсы или испытывали трудности с модификацией типов, эти пять важнейших утилитарных типов изменят то, как вы пишете код на TypeScript.

Ключевые выводы

  • Утилитарные типы преобразуют существующие типы без их переписывания, сокращая дублирование кода
  • Partial делает все свойства опциональными, идеально подходит для операций обновления
  • Required делает все свойства обязательными, обеспечивая полноту структур данных
  • Pick и Omit помогают создавать сфокусированные интерфейсы путем выбора или исключения свойств
  • Readonly предотвращает случайные мутации, необходим для паттернов неизменяемых данных

Что такое утилитарные типы TypeScript и почему они важны

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

Мы рассмотрим пять фундаментальных утилитарных типов, которые решают распространенные реальные проблемы: Partial, Required, Pick, Omit и Readonly. Каждый из них решает конкретные сценарии, с которыми вы сталкиваетесь ежедневно при разработке на TypeScript.

Partial<T>: делаем все свойства опциональными в TypeScript

Утилитарный тип Partial преобразует все свойства типа в опциональные, идеально подходит для операций обновления, где вы изменяете только определенные поля.

Когда использовать Partial для обновления форм

Рассмотрим обновление профиля пользователя, где пользователи могут изменять отдельные поля без отправки всего объекта:

interface User {
  id: string;
  name: string;
  email: string;
  avatar: string;
  lastLogin: Date;
}

function updateUserProfile(userId: string, updates: Partial<User>) {
  // Обновляем только предоставленные поля
  return api.patch(`/users/${userId}`, updates);
}

// Использование - обновляем только email
updateUserProfile("123", { email: "new@email.com" });

Пример типа Partial с реальным кодом

Обновления состояния React значительно выигрывают от Partial:

const [user, setUser] = useState<User>(initialUser);

const updateUser = (updates: Partial<User>) => {
  setUser(prev => ({ ...prev, ...updates }));
};

// Обновляем только конкретные поля
updateUser({ name: "Jane Doe", avatar: "new-avatar.jpg" });

Required<T>: принуждение всех свойств в TypeScript

Required делает противоположное Partial — он делает все опциональные свойства обязательными, обеспечивая полноту структур данных.

Преобразование опциональных в обязательные свойства

Это оказывается бесценным для валидации конфигурации:

interface DatabaseConfig {
  host?: string;
  port?: number;
  username?: string;
  password?: string;
}

// Убеждаемся, что все значения конфигурации существуют перед подключением
type ValidatedConfig = Required<DatabaseConfig>;

function connectDatabase(config: ValidatedConfig) {
  // Все свойства гарантированно существуют
  return createConnection(config);
}

Тип Required в действии

Отправка форм часто требует полных данных:

interface RegistrationForm {
  username?: string;
  email?: string;
  password?: string;
  terms?: boolean;
}

type CompleteRegistration = Required<RegistrationForm>;

function submitRegistration(data: CompleteRegistration) {
  // Все поля должны быть заполнены
  api.post('/register', data);
}

Pick<T, K>: выбор конкретных свойств из типов

Pick создает новый тип, выбирая только указанные свойства, идеально подходит для создания сфокусированных интерфейсов.

Создание сфокусированных подмножеств типов с Pick

Извлекайте только то, что нужно для конкретных компонентов:

interface Article {
  id: string;
  title: string;
  content: string;
  author: string;
  publishedAt: Date;
  tags: string[];
}

// Компонент карточки нуждается только в этих полях
type ArticlePreview = Pick<Article, 'id' | 'title' | 'author'>;

function ArticleCard({ article }: { article: ArticlePreview }) {
  return (
    <div>
      <h3>{article.title}</h3>
      <p>автор: {article.author}</p>
    </div>
  );
}

Pick против Omit: выбор правильного подхода

Используйте Pick, когда нужно несколько свойств, Omit — когда исключаете несколько свойств:

// Pick: Когда нужно 2-3 свойства из большого типа
type UserSummary = Pick<User, 'id' | 'name'>;

// Omit: Когда нужны большинство свойств кроме нескольких
type PublicUser = Omit<User, 'password'>;

Omit<T, K>: исключение свойств из типов TypeScript

Omit создает тип, исключая указанные свойства, идеально подходит для удаления чувствительных или внутренних полей.

Удаление чувствительных данных с Omit

Защитите чувствительную информацию в ответах API:

interface UserAccount {
  id: string;
  email: string;
  password: string;
  creditCard: string;
  publicProfile: boolean;
}

// Удаляем чувствительные поля для публичного API
type PublicUserData = Omit<UserAccount, 'password' | 'creditCard'>;

function getPublicProfile(userId: string): Promise<PublicUserData> {
  return api.get(`/users/${userId}/public`);
}

Лучшие практики утилитарного типа Omit

Комбинируйте с другими утилитами для мощных паттернов:

// Удаляем внутренние поля и делаем оставшиеся опциональными
type UserUpdatePayload = Partial<Omit<User, 'id' | 'createdAt'>>;

Readonly<T>: создание неизменяемых типов в TypeScript

Readonly делает все свойства неизменяемыми, предотвращая случайные мутации и обеспечивая целостность данных.

Предотвращение случайных мутаций с Readonly

Необходимо для управления состоянием и конфигурации:

interface AppConfig {
  apiEndpoint: string;
  version: string;
  features: string[];
}

type ImmutableConfig = Readonly<AppConfig>;

const config: ImmutableConfig = {
  apiEndpoint: "https://api.example.com",
  version: "1.0.0",
  features: ["auth", "payments"]
};

// Ошибка TypeScript: Cannot assign to 'apiEndpoint' because it is a read-only property
config.apiEndpoint = "https://new-api.com"; // ❌

Readonly свойства и React Props

Обеспечьте неизменность пропсов:

type UserCardProps = Readonly<{
  user: User;
  onSelect: (id: string) => void;
}>;

function UserCard({ user, onSelect }: UserCardProps) {
  // Пропсы неизменяемы внутри компонента
  return <div onClick={() => onSelect(user.id)}>{user.name}</div>;
}

Комбинирование нескольких утилитарных типов для продвинутых паттернов

Утилитарные типы становятся еще более мощными при комбинировании:

interface DatabaseRecord {
  id: string;
  createdAt: Date;
  updatedAt: Date;
  deletedAt?: Date;
  data: Record<string, unknown>;
}

// Создаем тип для новых записей: без временных меток, частичные данные
type NewRecord = Omit<DatabaseRecord, 'id' | 'createdAt' | 'updatedAt'> & {
  data: Partial<DatabaseRecord['data']>;
};

// Тип обновления: все опционально кроме ID
type UpdateRecord = Partial<Omit<DatabaseRecord, 'id'>> & Pick<DatabaseRecord, 'id'>;

Заключение

Эти пять утилитарных типов — Partial, Required, Pick, Omit и Readonly — формируют основу эффективной разработки на TypeScript. Они устраняют повторяющиеся определения типов, обеспечивают типобезопасность и делают ваш код более поддерживаемым.

Начните внедрять эти паттерны в вашу кодовую базу. Начните с Partial для операций обновления, используйте Omit для создания безопасных публичных интерфейсов и применяйте Readonly для предотвращения ошибок. По мере освоения комбинируйте их для создания сложных преобразований типов, которые идеально соответствуют потребностям вашего приложения.

Часто задаваемые вопросы

Да, вы можете вкладывать утилитарные типы для создания сложных преобразований. Например, Partial и Omit хорошо работают вместе для создания полезных нагрузок обновления, которые исключают определенные поля, делая другие опциональными.

Нет, утилитарные типы являются чисто конструкциями времени компиляции. TypeScript удаляет всю информацию о типах во время компиляции, поэтому утилитарные типы имеют нулевые накладные расходы времени выполнения.

Расширение интерфейса создает новые типы путем добавления свойств, в то время как утилитарные типы преобразуют существующие типы. Утилитарные типы предлагают больше гибкости для модификации типов, которыми вы не управляете, или создания вариаций существующих типов.

Да, TypeScript позволяет создавать пользовательские утилитарные типы, используя отображаемые типы, условные типы и типы шаблонных литералов. Начните со встроенных утилит и создавайте пользовательские, когда выявляете повторяющиеся паттерны.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay