← TypeScript/Generic ограничения: extends и keyof#219 из 430← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: TypeScript setТермин: TypeScriptТема: TypeScript для frontend
← НазадДалее →

Generic ограничения: extends и keyof

Зачем нужны ограничения

Без ограничений generic-тип T может быть чем угодно, и TypeScript не позволит обращаться к его свойствам:

function getLength<T>(value: T): number {
  return value.length  // Ошибка TS: Property 'length' does not exist on type 'T'
}

// Решение — ограничить T типами у которых есть length:
function getLength<T extends { length: number }>(value: T): number {
  return value.length  // OK!
}

getLength('hello')       // 5 — string имеет length
getLength([1, 2, 3])     // 3 — array имеет length
// getLength(42)         // Ошибка TS: number не удовлетворяет { length: number }

T extends SomeType

extends задаёт минимальный контракт для T:

interface HasId {
  id: number
}

// T должен иметь поле id
function findById<T extends HasId>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id)
}

const users = [{ id: 1, name: 'Алексей' }, { id: 2, name: 'Ольга' }]
const found = findById(users, 1)  // { id: 1, name: 'Алексей' } — тип User, не HasId!

// Множественные ограничения через intersection:
function process<T extends HasId & { name: string }>(item: T): string {
  return `#${item.id}: ${item.name}`
}

keyof — ограничение ключами объекта

keyof T — тип, объединяющий все ключи T. Используется для безопасного обращения к свойствам:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]  // TypeScript знает точный тип T[K]
}

const user = { name: 'Алексей', age: 30, active: true }
const name   = getProperty(user, 'name')    // тип string
const age    = getProperty(user, 'age')     // тип number
// getProperty(user, 'email')  // Ошибка TS: 'email' не является ключом

function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
  return { ...obj, [key]: value }
}

Условные возвращаемые типы

Ограничения позволяют TypeScript выводить точные типы результата:

// Возвращаемый тип зависит от переданного ключа
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map(item => item[key])
}

const users = [
  { name: 'Алексей', age: 30 },
  { name: 'Ольга',   age: 25 },
]

const names = pluck(users, 'name')  // string[] — TypeScript знает тип!
const ages  = pluck(users, 'age')   // number[]

Практический пример: типобезопасный маппер

// Преобразуем массив объектов в Map по ключу
function groupBy<T, K extends keyof T>(
  items: T[],
  key: K
): Map<T[K], T[]> {
  const map = new Map<T[K], T[]>()
  for (const item of items) {
    const k = item[key]
    const group = map.get(k) ?? []
    map.set(k, [...group, item])
  }
  return map
}

const orders = [
  { id: 1, status: 'pending', amount: 100 },
  { id: 2, status: 'done',    amount: 200 },
  { id: 3, status: 'pending', amount: 50  },
]

const byStatus = groupBy(orders, 'status')
// Map { 'pending' => [{...}, {...}], 'done' => [{...}] }

Constraint с default-значением

// T по умолчанию object, но можно передать другой тип
function createStore<T extends object = Record<string, unknown>>(
  initial: T
): { get(): T; set(updates: Partial<T>): void } {
  let state = { ...initial }
  return {
    get: () => ({ ...state }),
    set: (updates) => { state = { ...state, ...updates } }
  }
}

Примеры

Runtime реализация getProperty, pluck и groupBy — типобезопасные операции с объектами в стиле TypeScript generics

// TypeScript: function getProperty<T, K extends keyof T>(obj: T, key: K): T[K]
// JS: реализуем с runtime-проверками

function getProperty(obj, key) {
  if (!Object.prototype.hasOwnProperty.call(obj, key)) {
    throw new RangeError(
      `Ключ "${String(key)}" не существует. Доступные: ${Object.keys(obj).join(', ')}`
    )
  }
  return obj[key]
}

function setProperty(obj, key, value) {
  if (!Object.prototype.hasOwnProperty.call(obj, key)) {
    throw new RangeError(`Ключ "${String(key)}" не существует в объекте`)
  }
  return { ...obj, [key]: value }
}

// TypeScript: function pluck<T, K extends keyof T>(items: T[], key: K): T[K][]
function pluck(items, key) {
  return items.map(item => {
    if (!Object.prototype.hasOwnProperty.call(item, key)) {
      throw new RangeError(`Элемент не имеет ключа "${String(key)}"`)
    }
    return item[key]
  })
}

// TypeScript: function groupBy<T, K extends keyof T>(items: T[], key: K): Map<T[K], T[]>
function groupBy(items, key) {
  const map = new Map()
  for (const item of items) {
    const k = item[key]
    if (!map.has(k)) map.set(k, [])
    map.get(k).push(item)
  }
  return map
}

// TypeScript: function sortBy<T, K extends keyof T>(items: T[], key: K): T[]
function sortBy(items, key) {
  return [...items].sort((a, b) => {
    const va = a[key]
    const vb = b[key]
    if (va < vb) return -1
    if (va > vb) return 1
    return 0
  })
}

// --- Демонстрация ---

const users = [
  { id: 3, name: 'Виктор',  age: 35, role: 'user'  },
  { id: 1, name: 'Алексей', age: 30, role: 'admin' },
  { id: 4, name: 'Мария',   age: 28, role: 'user'  },
  { id: 2, name: 'Ольга',   age: 25, role: 'admin' },
]

console.log('=== getProperty ===')
console.log(getProperty(users[0], 'name'))  // 'Виктор'
console.log(getProperty(users[0], 'age'))   // 35

try {
  getProperty(users[0], 'email')  // ключ не существует
} catch (e) {
  console.log('Ошибка:', e.message)
}

console.log('\n=== setProperty (иммутабельный) ===')
const updated = setProperty(users[0], 'age', 36)
console.log('Обновлённый:', updated.age)  // 36
console.log('Оригинал:', users[0].age)   // 35 — не изменился

console.log('\n=== pluck ===')
const names = pluck(users, 'name')
console.log('Имена:', names)

const ages = pluck(users, 'age')
console.log('Возрасты:', ages)

console.log('\n=== groupBy ===')
const byRole = groupBy(users, 'role')
console.log('Администраторы:', byRole.get('admin').map(u => u.name))
console.log('Пользователи:', byRole.get('user').map(u => u.name))

console.log('\n=== sortBy ===')
const byAge  = sortBy(users, 'age')
const byName = sortBy(users, 'name')
console.log('По возрасту:', pluck(byAge, 'name'))
console.log('По имени:', pluck(byName, 'name'))

Generic ограничения: extends и keyof

Зачем нужны ограничения

Без ограничений generic-тип T может быть чем угодно, и TypeScript не позволит обращаться к его свойствам:

function getLength<T>(value: T): number {
  return value.length  // Ошибка TS: Property 'length' does not exist on type 'T'
}

// Решение — ограничить T типами у которых есть length:
function getLength<T extends { length: number }>(value: T): number {
  return value.length  // OK!
}

getLength('hello')       // 5 — string имеет length
getLength([1, 2, 3])     // 3 — array имеет length
// getLength(42)         // Ошибка TS: number не удовлетворяет { length: number }

T extends SomeType

extends задаёт минимальный контракт для T:

interface HasId {
  id: number
}

// T должен иметь поле id
function findById<T extends HasId>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id)
}

const users = [{ id: 1, name: 'Алексей' }, { id: 2, name: 'Ольга' }]
const found = findById(users, 1)  // { id: 1, name: 'Алексей' } — тип User, не HasId!

// Множественные ограничения через intersection:
function process<T extends HasId & { name: string }>(item: T): string {
  return `#${item.id}: ${item.name}`
}

keyof — ограничение ключами объекта

keyof T — тип, объединяющий все ключи T. Используется для безопасного обращения к свойствам:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]  // TypeScript знает точный тип T[K]
}

const user = { name: 'Алексей', age: 30, active: true }
const name   = getProperty(user, 'name')    // тип string
const age    = getProperty(user, 'age')     // тип number
// getProperty(user, 'email')  // Ошибка TS: 'email' не является ключом

function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
  return { ...obj, [key]: value }
}

Условные возвращаемые типы

Ограничения позволяют TypeScript выводить точные типы результата:

// Возвращаемый тип зависит от переданного ключа
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map(item => item[key])
}

const users = [
  { name: 'Алексей', age: 30 },
  { name: 'Ольга',   age: 25 },
]

const names = pluck(users, 'name')  // string[] — TypeScript знает тип!
const ages  = pluck(users, 'age')   // number[]

Практический пример: типобезопасный маппер

// Преобразуем массив объектов в Map по ключу
function groupBy<T, K extends keyof T>(
  items: T[],
  key: K
): Map<T[K], T[]> {
  const map = new Map<T[K], T[]>()
  for (const item of items) {
    const k = item[key]
    const group = map.get(k) ?? []
    map.set(k, [...group, item])
  }
  return map
}

const orders = [
  { id: 1, status: 'pending', amount: 100 },
  { id: 2, status: 'done',    amount: 200 },
  { id: 3, status: 'pending', amount: 50  },
]

const byStatus = groupBy(orders, 'status')
// Map { 'pending' => [{...}, {...}], 'done' => [{...}] }

Constraint с default-значением

// T по умолчанию object, но можно передать другой тип
function createStore<T extends object = Record<string, unknown>>(
  initial: T
): { get(): T; set(updates: Partial<T>): void } {
  let state = { ...initial }
  return {
    get: () => ({ ...state }),
    set: (updates) => { state = { ...state, ...updates } }
  }
}

Примеры

Runtime реализация getProperty, pluck и groupBy — типобезопасные операции с объектами в стиле TypeScript generics

// TypeScript: function getProperty<T, K extends keyof T>(obj: T, key: K): T[K]
// JS: реализуем с runtime-проверками

function getProperty(obj, key) {
  if (!Object.prototype.hasOwnProperty.call(obj, key)) {
    throw new RangeError(
      `Ключ "${String(key)}" не существует. Доступные: ${Object.keys(obj).join(', ')}`
    )
  }
  return obj[key]
}

function setProperty(obj, key, value) {
  if (!Object.prototype.hasOwnProperty.call(obj, key)) {
    throw new RangeError(`Ключ "${String(key)}" не существует в объекте`)
  }
  return { ...obj, [key]: value }
}

// TypeScript: function pluck<T, K extends keyof T>(items: T[], key: K): T[K][]
function pluck(items, key) {
  return items.map(item => {
    if (!Object.prototype.hasOwnProperty.call(item, key)) {
      throw new RangeError(`Элемент не имеет ключа "${String(key)}"`)
    }
    return item[key]
  })
}

// TypeScript: function groupBy<T, K extends keyof T>(items: T[], key: K): Map<T[K], T[]>
function groupBy(items, key) {
  const map = new Map()
  for (const item of items) {
    const k = item[key]
    if (!map.has(k)) map.set(k, [])
    map.get(k).push(item)
  }
  return map
}

// TypeScript: function sortBy<T, K extends keyof T>(items: T[], key: K): T[]
function sortBy(items, key) {
  return [...items].sort((a, b) => {
    const va = a[key]
    const vb = b[key]
    if (va < vb) return -1
    if (va > vb) return 1
    return 0
  })
}

// --- Демонстрация ---

const users = [
  { id: 3, name: 'Виктор',  age: 35, role: 'user'  },
  { id: 1, name: 'Алексей', age: 30, role: 'admin' },
  { id: 4, name: 'Мария',   age: 28, role: 'user'  },
  { id: 2, name: 'Ольга',   age: 25, role: 'admin' },
]

console.log('=== getProperty ===')
console.log(getProperty(users[0], 'name'))  // 'Виктор'
console.log(getProperty(users[0], 'age'))   // 35

try {
  getProperty(users[0], 'email')  // ключ не существует
} catch (e) {
  console.log('Ошибка:', e.message)
}

console.log('\n=== setProperty (иммутабельный) ===')
const updated = setProperty(users[0], 'age', 36)
console.log('Обновлённый:', updated.age)  // 36
console.log('Оригинал:', users[0].age)   // 35 — не изменился

console.log('\n=== pluck ===')
const names = pluck(users, 'name')
console.log('Имена:', names)

const ages = pluck(users, 'age')
console.log('Возрасты:', ages)

console.log('\n=== groupBy ===')
const byRole = groupBy(users, 'role')
console.log('Администраторы:', byRole.get('admin').map(u => u.name))
console.log('Пользователи:', byRole.get('user').map(u => u.name))

console.log('\n=== sortBy ===')
const byAge  = sortBy(users, 'age')
const byName = sortBy(users, 'name')
console.log('По возрасту:', pluck(byAge, 'name'))
console.log('По имени:', pluck(byName, 'name'))

Задание

Реализуй функцию filterByProperty(items, key, value), которая фильтрует массив объектов по значению указанного ключа. Реализуй функцию uniqueBy(items, key), которая удаляет дубликаты из массива объектов по значению ключа (сохраняет первое вхождение). Обе функции не должны мутировать исходный массив.

Подсказка

filterByProperty: return items.filter(item => item[key] === value). uniqueBy: const seen = new Set(); return items.filter(item => { const v = item[key]; if (seen.has(v)) return false; seen.add(v); return true; }).

Загружаем среду выполнения...
Загружаем AI-помощника...