← Курс/Indexed Access Types: T[K]#149 из 257+25 XP

Indexed Access Types: T[K]

Что такое Indexed Access Types

Indexed Access Types позволяют получить **тип конкретного свойства** из другого типа, используя синтаксис квадратных скобок T[K]. Это похоже на обращение к свойству объекта, но на уровне типов.

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

// Получаем тип конкретного свойства:
type UserName = User['name']   // type UserName = string
type UserId = User['id']       // type UserId = number
type UserAge = User['age']     // type UserAge = number

Использование с keyof

Комбинация с keyof позволяет получить тип **любого** свойства:

type UserKey = keyof User
// type UserKey = 'id' | 'name' | 'email' | 'age'

type UserValue = User[keyof User]
// type UserValue = number | string
// Объединение всех возможных типов значений

// Практический паттерн — функция getProperty:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user: User = { id: 1, name: 'Алексей', email: 'a@b.com', age: 30 }
const name = getProperty(user, 'name')   // тип: string
const id = getProperty(user, 'id')       // тип: number

Indexed access с массивами: T[number]

Для массивов можно использовать number как индекс для получения типа элемента:

type Colors = ['red', 'green', 'blue']

type FirstColor = Colors[0]   // type FirstColor = 'red'
type SecondColor = Colors[1]  // type SecondColor = 'green'

// T[number] — тип любого элемента массива:
type AnyColor = Colors[number]  // type AnyColor = 'red' | 'green' | 'blue'

// Особенно полезно с as const:
const ROUTES = ['/home', '/about', '/contact'] as const
type Route = typeof ROUTES[number]
// type Route = '/home' | '/about' | '/contact'

Вложенный доступ

interface Config {
  server: {
    host: string
    port: number
    ssl: {
      enabled: boolean
      cert: string
    }
  }
  database: {
    url: string
    maxConnections: number
  }
}

type ServerConfig = Config['server']
// type ServerConfig = { host: string; port: number; ssl: { ... } }

type ServerPort = Config['server']['port']
// type ServerPort = number

type SSLEnabled = Config['server']['ssl']['enabled']
// type SSLEnabled = boolean

type DbUrl = Config['database']['url']
// type DbUrl = string

Получение нескольких типов сразу

interface Product {
  id: number
  name: string
  price: number
  category: string
}

// Получить типы нескольких свойств через union:
type ProductStrings = Product['name' | 'category']
// type ProductStrings = string

type ProductNumbers = Product['id' | 'price']
// type ProductNumbers = number

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

// API-функция которая возвращает только нужные поля:
type ApiResponse<T, K extends keyof T> = {
  data: Pick<T, K>
  status: number
}

// Значение enum из массива:
const PERMISSIONS = ['read', 'write', 'admin'] as const
type Permission = typeof PERMISSIONS[number]
// type Permission = 'read' | 'write' | 'admin'

function hasPermission(user: { permissions: Permission[] }, perm: Permission) {
  return user.permissions.includes(perm)
}

Примеры

Indexed access паттерны: динамический доступ к свойствам с проверкой ключей

// В TS: T[K] — получить тип свойства K из типа T
// В JS: показываем runtime-аналог с проверкой корректности ключей

// === Базовый indexed access (getProperty) ===
function getProperty(obj, key) {
  // В TS: function getProperty<T, K extends keyof T>(obj: T, key: K): T[K]
  if (!(key in obj)) {
    throw new Error(`Свойство '${key}' не существует на объекте`)
  }
  return obj[key]
}

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

console.log('=== getProperty ===')
console.log(getProperty(user, 'name'))   // 'Алексей'
console.log(getProperty(user, 'id'))     // 1
console.log(getProperty(user, 'age'))    // 30

try {
  getProperty(user, 'phone')  // В TS: ошибка компиляции. В JS: runtime ошибка
} catch (e) {
  console.log(e.message)  // "Свойство 'phone' не существует на объекте"
}

// === T[number] — тип элемента массива (через as const паттерн) ===
console.log('\n=== Тип элемента массива ===')

const ROUTES = ['/home', '/about', '/contact', '/profile']
// В TS: type Route = typeof ROUTES[number] = '/home' | '/about' | ...

function isValidRoute(route) {
  // runtime проверка того, что TS делает через typeof ROUTES[number]
  return ROUTES.includes(route)
}

console.log(isValidRoute('/home'))       // true
console.log(isValidRoute('/about'))      // true
console.log(isValidRoute('/unknown'))    // false

// === Вложенный indexed access ===
console.log('\n=== Вложенный доступ ===')

const config = {
  server: { host: 'localhost', port: 3000, ssl: { enabled: false, cert: '' } },
  database: { url: 'postgres://...', maxConnections: 10 }
}

// В TS: type ServerPort = Config['server']['port'] = number
function getNestedValue(obj, ...keys) {
  return keys.reduce((current, key) => {
    if (current == null || !(key in current)) {
      throw new Error(`Путь '${keys.join('.')}' не существует`)
    }
    return current[key]
  }, obj)
}

console.log(getNestedValue(config, 'server', 'port'))          // 3000
console.log(getNestedValue(config, 'server', 'ssl', 'enabled')) // false
console.log(getNestedValue(config, 'database', 'url'))          // 'postgres://...'

// === Получение всех значений объекта (T[keyof T]) ===
console.log('\n=== Все значения объекта ===')

const STATUS = { PENDING: 'pending', ACTIVE: 'active', CLOSED: 'closed' }
// В TS: type StatusValue = typeof STATUS[keyof typeof STATUS] = 'pending' | 'active' | 'closed'

const statusValues = Object.values(STATUS)
console.log('Допустимые статусы:', statusValues)  // ['pending', 'active', 'closed']

function validateStatus(status) {
  if (!statusValues.includes(status)) {
    throw new Error(`Недопустимый статус: ${status}`)
  }
  return status
}

console.log(validateStatus('active'))   // 'active'
try {
  validateStatus('unknown')
} catch (e) {
  console.log(e.message)  // 'Недопустимый статус: unknown'
}