← Курс/Паттерны типизации объектов#155 из 257+25 XP

Паттерны типизации объектов

Partial — все поля опциональны

Partial<T> создаёт тип, где все свойства T становятся необязательными. Идеально для функций обновления:

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

// Partial<User> = { id?: number; name?: string; email?: string; age?: number }

function updateUser(user: User, changes: Partial<User>): User {
  return { ...user, ...changes }
}

updateUser(user, { name: 'Новое имя' })      // OK — только name
updateUser(user, { age: 30, email: 'x@y.z' }) // OK — несколько полей
updateUser(user, {})                           // OK — ничего не меняем

Required — все поля обязательны

Required<T> — обратное Partial. Убирает опциональность у всех свойств:

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

type StrictConfig = Required<Config>
// { host: string; port: number; debug: boolean } — всё обязательно

function initApp(config: Required<Config>) { /* ... */ }

Pick — выбор нужных полей

Pick<T, K> создаёт тип только с указанными ключами:

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

// Публичные данные (без password):
type PublicUser = Pick<User, 'id' | 'name' | 'email'>
// { id: number; name: string; email: string }

// Только для отображения в списке:
type UserListItem = Pick<User, 'id' | 'name'>
// { id: number; name: string }

Omit — исключение полей

Omit<T, K> — обратное Pick. Создаёт тип без указанных ключей:

// Пользователь для создания (без id и createdAt — они генерируются):
type CreateUserDto = Omit<User, 'id' | 'createdAt'>
// { name: string; email: string; password: string }

// Без чувствительных данных:
type SafeUser = Omit<User, 'password'>
// { id: number; name: string; email: string; createdAt: Date }

Record — словарь с типизированными ключами и значениями

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

type HttpStatus = 200 | 400 | 404 | 500

const statusMessages: Record<HttpStatus, string> = {
  200: 'OK',
  400: 'Bad Request',
  404: 'Not Found',
  500: 'Internal Server Error',
}

// Словарь пользователей по id:
type UserCache = Record<number, User>
const cache: UserCache = {}
cache[1] = { id: 1, name: 'Алексей', ... }

// Счётчики по строковым ключам:
type Counters = Record<string, number>
const counters: Counters = {}
counters['clicks'] = 5

Intersection types для mixins

Пересечение типов (&) объединяет несколько типов в один:

interface WithId {
  id: number
}

interface WithTimestamps {
  createdAt: Date
  updatedAt: Date
}

interface WithSoftDelete {
  deletedAt: Date | null
}

// Модель базы данных со всеми полями:
type DbModel = WithId & WithTimestamps & WithSoftDelete & {
  // специфичные поля
}

// Функция принимает любой объект с id:
function findById<T extends WithId>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id)
}

Комбинирование утилит

Утилитные типы можно комбинировать:

// Форма создания: без id, все поля обязательны
type CreateDto<T> = Required<Omit<T, 'id'>>

// Форма обновления: без id, все поля опциональны
type UpdateDto<T> = Partial<Omit<T, 'id'>>

type CreateUser = CreateDto<User>   // { name: string; email: string; ... }
type UpdateUser = UpdateDto<User>   // { name?: string; email?: string; ... }

Примеры

Паттерны Partial, Required, Pick, Omit, Record через JavaScript функции

// В TS: Partial<T>, Pick<T, K>, Omit<T, K>, Record<K, V> — утилитные типы
// В JS: реализуем их как runtime-функции

// === Partial: обновление объекта ===
function updateObject(original, changes) {
  // В TS: function update<T>(obj: T, changes: Partial<T>): T
  return { ...original, ...changes }
}

const user = { id: 1, name: 'Алексей', email: 'alex@test.com', age: 28 }

console.log('=== Partial (updateObject) ===')
console.log(updateObject(user, { name: 'Дмитрий' }))
// { id: 1, name: 'Дмитрий', email: 'alex@test.com', age: 28 }
console.log(updateObject(user, { age: 30, email: 'new@test.com' }))
// { id: 1, name: 'Алексей', email: 'new@test.com', age: 30 }

// === Pick: выбрать только нужные поля ===
function pick(obj, keys) {
  // В TS: function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>
  return keys.reduce((result, key) => {
    if (key in obj) result[key] = obj[key]
    return result
  }, {})
}

console.log('\n=== Pick ===')
const publicUser = pick(user, ['id', 'name'])
console.log(publicUser)  // { id: 1, name: 'Алексей' }

const userPreview = pick(user, ['id', 'name', 'email'])
console.log(userPreview)  // { id: 1, name: 'Алексей', email: 'alex@test.com' }

// === Omit: исключить ненужные поля ===
function omit(obj, keys) {
  // В TS: function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K>
  return Object.fromEntries(
    Object.entries(obj).filter(([key]) => !keys.includes(key))
  )
}

console.log('\n=== Omit ===')
const userWithoutAge = omit(user, ['age'])
console.log(userWithoutAge)  // { id: 1, name: 'Алексей', email: 'alex@test.com' }

// Убрать id для DTO создания:
const createUserDto = omit(user, ['id'])
console.log(createUserDto)  // { name: 'Алексей', email: 'alex@test.com', age: 28 }

// === Record: словарь с типизированными ключами ===
console.log('\n=== Record ===')

const statusMessages = {
  200: 'OK',
  201: 'Created',
  400: 'Bad Request',
  401: 'Unauthorized',
  404: 'Not Found',
  500: 'Internal Server Error',
}
// В TS: Record<200|201|400|401|404|500, string>

function getStatusMessage(code) {
  return statusMessages[code] ?? `Неизвестный код: ${code}`
}

[200, 404, 500, 418].forEach(code => {
  console.log(`${code}: ${getStatusMessage(code)}`)
})

// === Intersection (мixin паттерн) ===
console.log('\n=== Intersection (mixin) ===')

function withTimestamps(obj) {
  // В TS: T & { createdAt: Date; updatedAt: Date }
  return { ...obj, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }
}

function withSoftDelete(obj) {
  // В TS: T & { deletedAt: Date | null }
  return { ...obj, deletedAt: null }
}

const dbRecord = withSoftDelete(withTimestamps({ id: 1, name: 'Тест' }))
console.log(Object.keys(dbRecord))  // ['id', 'name', 'createdAt', 'updatedAt', 'deletedAt']