Avatar ·

Как исключить свойство из типа в TypeScript (Exclude property from type)

Привет всем! Я новичок в TypeScript и столкнулся с проблемой при работе с типами объектов. У меня есть интерфейс `User`, который содержит много полей: `id`, `name`, `email`, `password`, `createdAt`, `role` и т.д. Когда я возвращаю данные пользователя из API, я не хочу отдавать клиенту `password` и `createdAt` (это внутренние поля). Проблема в том, что я не хочу создавать отдельный интерфейс для публичного представления пользователя, потому что это приводит к дублированию кода. Если я добавлю новое поле в `User`, мне придется не забыть добавить его и в публичный интерфейс. Вот пример: ```typescript interface User { id: number; name: string; email: string; password: string; createdAt: Date; role: 'admin' | 'user'; } // Как сделать так, чтобы получить тип без password и createdAt? // Хочется что-то вроде: // type PublicUser = ExcludeProperty; ``` Я пробовал использовать `Omit`, но не уверен, что это правильный подход. Может быть есть другие способы? Или `Omit` — это стандартное решение? Подскажите, пожалуйста, как правильно исключать свойства из типа в TypeScript, чтобы код был чистым и переиспользуемым. <<>>

Использование встроенного типа Omit

Да, Omit<T, K> — это стандартное и наиболее распространенное решение для исключения свойств из типа. Он встроен в TypeScript начиная с версии 3.5. Ваш пример будет работать именно так, как вы ожидаете:

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

// Исключаем password и createdAt
type PublicUser = Omit<User, 'password' | 'createdAt'>;

// Теперь PublicUser эквивалентен:
// {
//   id: number;
//   name: string;
//   email: string;
//   role: 'admin' | 'user';
// }

Как это работает под капотом

Omit — это комбинация двух других утилитарных типов: Pick и Exclude. Его определение выглядит так:

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
  • keyof T — получает объединение всех ключей типа T (например, 'id' | 'name' | 'email' | 'password' | 'createdAt' | 'role').
  • Exclude<keyof T, K> — исключает из этого объединения ключи, переданные в K (получаем 'id' | 'name' | 'email' | 'role').
  • Pick<T, ...> — выбирает из T только те свойства, ключи которых остались после исключения.

Практический пример с функцией

Вот как это можно использовать в реальном коде для безопасного возврата данных из API:

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

// Тип для публичных данных пользователя
type PublicUser = Omit<User, 'password' | 'createdAt'>;

// Функция, которая "чистит" пользователя перед отправкой
function toPublicUser(user: User): PublicUser {
  // Деструктуризация с исключением ненужных полей
  const { password, createdAt, ...publicData } = user;
  return publicData; // TypeScript проверит, что publicData соответствует PublicUser
}

// Использование
const user: User = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  password: 'secret123',
  createdAt: new Date(),
  role: 'user'
};

const publicUser = toPublicUser(user);
console.log(publicUser); // { id: 1, name: 'Alice', email: 'alice@example.com', role: 'user' }
// password и createdAt отсутствуют

Альтернативы и расширенные случаи

Если вам нужно исключить свойства по их типу (а не по имени), можно написать свой утилитарный тип:

// Исключаем все свойства, которые являются функциями
type ExcludeFunctionProperties<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K]
};

interface Service {
  name: string;
  start(): void;
  stop(): void;
  version: number;
}

type ServiceWithoutMethods = ExcludeFunctionProperties<Service>;
// { name: string; version: number; }

Но для 95% случаев Omit — это именно то, что вам нужно. Он лаконичен, понятен и полностью типобезопасен. Вы также можете комбинировать его с другими типами:

// Исключаем свойства и делаем остальные readonly
type ReadonlyPublicUser = Readonly<Omit<User, 'password' | 'createdAt'>>;

Важные замечания

  • Omit работает только с плоскими объектами. Для вложенных объектов нужно применять его рекурсивно или использовать библиотеки вроде type-fest.
  • Если вы попытаетесь исключить свойство, которого нет в типе, TypeScript выдаст ошибку — это защита от опечаток.
  • Для интерфейсов и типов Omit работает одинаково хорошо.

Используйте Omit везде, где нужно создать "подтип" существующего типа, исключив некоторые поля. Это гораздо лучше, чем дублировать интерфейсы вручную!

<<>>

Создание собственного утилитарного типа ExcludeProperty

Если вам не нравится название Omit (оно может быть неочевидным для новичков) или вы хотите добавить дополнительную логику, можно легко создать свой тип:

// Собственная реализация с понятным названием
type ExcludeProperty<T, K extends keyof T> = Omit<T, K>;

// Или с дополнительной валидацией
type ExcludePropertyStrict<T, K extends keyof T> = {
  [P in Exclude<keyof T, K>]: T[P]
};

// Использование
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

type PublicUser = ExcludeProperty<User, 'password'>;
// { id: number; name: string; email: string; }

Исключение свойств по условию

Иногда нужно исключать не по имени, а по значению. Например, убрать все поля, которые могут быть null или undefined:

type ExcludeNullable<T> = {
  [K in keyof T as null extends T[K] ? never : K]: T[K]
};

interface Config {
  url: string;
  timeout?: number; // может быть undefined
  retries: number | null; // может быть null
}

type StrictConfig = ExcludeNullable<Config>;
// { url: string; } — timeout и retries исключены

Такой подход полезен, когда вы работаете с данными из ненадежных источников и хотите гарантировать, что в итоговом типе останутся только обязательные не-null поля. Однако для простого исключения конкретных свойств Omit остается лучшим выбором.