← Курс/Utility Types#171 из 257+35 XP

Utility Types — встроенные утилиты TypeScript

TypeScript предоставляет готовые утилиты для трансформации типов. Все они реализованы через mapped types и conditional types.

Partial<T> и Required<T>

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

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

// Все поля обязательны (убирает ?)
type UserRequired = Required<Partial<User>>
// { id: number; name: string; ... }

// Практика — обновление записи:
function updateUser(user: User, changes: Partial<User>): User {
  return { ...user, ...changes }
}

Как реализовано: type Partial<T> = { [K in keyof T]?: T[K] }

Readonly<T>

type ReadonlyUser = Readonly<User>
// Все поля readonly — нельзя изменить после создания

const user: ReadonlyUser = { id: 1, name: 'Алексей', email: 'a@b.ru', age: 30 }
// user.name = 'Другой'  // Ошибка TS: Cannot assign to 'name' because it is a read-only property

Record<K, V>

Создаёт тип объекта с ключами K и значениями V:

type PageViews = Record<string, number>
const views: PageViews = { '/home': 100, '/about': 50 }

type UserRoles = Record<'admin' | 'user' | 'guest', string[]>
const roles: UserRoles = {
  admin: ['read', 'write', 'delete'],
  user:  ['read', 'write'],
  guest: ['read'],
}

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

Pick<T, Keys> и Omit<T, Keys>

interface Article {
  id: number
  title: string
  content: string
  authorId: number
  publishedAt: Date
  tags: string[]
}

// Берём только нужные поля:
type ArticlePreview = Pick<Article, 'id' | 'title' | 'publishedAt'>

// Убираем ненужные поля:
type ArticleWithoutContent = Omit<Article, 'content'>

// Полезно для форм:
type CreateArticleForm = Omit<Article, 'id' | 'publishedAt'>

Exclude<T, U> и Extract<T, U>

Работают с union types:

type Status = 'pending' | 'success' | 'error' | 'cancelled'

// Убираем из union:
type ActiveStatus = Exclude<Status, 'cancelled'>
// 'pending' | 'success' | 'error'

// Оставляем только совпадающие:
type FinalStatus = Extract<Status, 'success' | 'error'>
// 'success' | 'error'

// NonNullable — убирает null и undefined:
type Name = string | null | undefined
type DefiniteName = NonNullable<Name>  // string

ReturnType<F>, Parameters<F>, InstanceType<C>

function createUser(name: string, age: number): User {
  return { id: Date.now(), name, email: '', age }
}

type UserType      = ReturnType<typeof createUser>   // User
type CreateParams  = Parameters<typeof createUser>   // [string, number]

class UserService {
  findById(id: number): User { /* ... */ }
}

type ServiceInstance = InstanceType<typeof UserService>
// UserService — тип экземпляра класса

Как работают mapped types (внутреннее устройство)

// Partial реализован так:
type MyPartial<T> = {
  [K in keyof T]?: T[K]
  // K перебирает все ключи T
  // ? делает поле опциональным
  // T[K] — тип значения для ключа K
}

// Pick реализован так:
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]
  // Перебираем только ключи из K
}

// Record реализован так:
type MyRecord<K extends keyof any, V> = {
  [P in K]: V
}

Практический пример: API с Partial

interface Config {
  apiUrl:  string
  timeout: number
  retries: number
  debug:   boolean
}

// Дефолтные значения:
const defaults: Config = {
  apiUrl:  'https://api.example.com',
  timeout: 5000,
  retries: 3,
  debug:   false,
}

// Пользователь передаёт только то что хочет изменить:
function createConfig(overrides: Partial<Config> = {}): Config {
  return { ...defaults, ...overrides }
}

const config = createConfig({ debug: true, timeout: 10000 })
// { apiUrl: 'https://...', timeout: 10000, retries: 3, debug: true }

Примеры

Реализация pick, omit, merge как функции. Практичный пример: данные из API + merge с дефолтами через Partial-паттерн

// В TypeScript: Pick<T, keys>, Omit<T, keys>, Partial<T>
// В JS — реализуем как функции

function pick(obj, keys) {
  return keys.reduce((acc, key) => {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      acc[key] = obj[key]
    }
    return acc
  }, {})
}

function omit(obj, keys) {
  const keySet = new Set(keys)
  return Object.fromEntries(
    Object.entries(obj).filter(([k]) => !keySet.has(k))
  )
}

// Merge: накладываем partial поверх base (только существующие ключи)
function mergePartial(base, partial) {
  const result = { ...base }
  for (const key of Object.keys(partial)) {
    if (Object.prototype.hasOwnProperty.call(base, key)) {
      result[key] = partial[key]
    }
  }
  return result
}

// Merge: сливаем все ключи (расширяющий merge)
function merge(base, partial) {
  return { ...base, ...partial }
}

// Readonly-заморозка объекта (как Readonly<T>)
function readonly(obj) {
  return Object.freeze({ ...obj })
}

// required — убеждаемся что все поля непустые
function assertRequired(obj, requiredKeys) {
  const missing = requiredKeys.filter(k => obj[k] == null)
  if (missing.length > 0) {
    throw new Error(`Отсутствуют обязательные поля: ${missing.join(', ')}`)
  }
  return obj
}

// --- Практичный пример: API данные + дефолты ---

const userDefaults = {
  id:        null,
  name:      'Аноним',
  email:     '',
  age:       0,
  role:      'user',
  active:    true,
  createdAt: new Date().toISOString(),
}

// Данные пришли из API — могут быть неполными
const apiResponse = {
  id:   42,
  name: 'Алексей Петров',
  email: 'alex@mail.ru',
}

// Применяем паттерн Partial<User>: только переданные поля перезаписывают дефолты
const fullUser = merge(userDefaults, apiResponse)
console.log('=== Полный объект пользователя ===')
console.log(fullUser)

// Pick — берём только нужные поля для карточки профиля
const profileCard = pick(fullUser, ['id', 'name', 'email', 'role'])
console.log('\n=== Карточка профиля (Pick) ===')
console.log(profileCard)

// Omit — убираем служебные поля перед отправкой
const publicUser = omit(fullUser, ['createdAt', 'active'])
console.log('\n=== Публичные данные (Omit) ===')
console.log(publicUser)

// Readonly — замораживаем конфигурацию
const config = readonly({
  apiUrl:  'https://api.example.com',
  timeout: 5000,
  retries: 3,
})
console.log('\n=== Readonly конфигурация ===')
console.log(config)

try {
  config.timeout = 10000  // В strict mode — TypeError, иначе тихо игнорируется
} catch (e) {
  console.log(`Нельзя изменить readonly: ${e.message}`)
}

// assertRequired — валидация
console.log('\n=== Валидация required полей ===')
try {
  assertRequired({ name: 'Алексей', email: null }, ['name', 'email', 'id'])
} catch (e) {
  console.log(`Ошибка: ${e.message}`)  // 'Отсутствуют обязательные поля: email, id'
}

const validated = assertRequired(
  { name: 'Алексей', email: 'a@b.ru', id: 1 },
  ['name', 'email', 'id']
)
console.log(`Валидация прошла: ${validated.name}`)