← Курс/typeof и keyof операторы#150 из 257+20 XP

typeof и keyof операторы

typeof в TypeScript

В JavaScript typeof — это runtime-оператор для проверки типа значения. В TypeScript typeof **дополнительно** работает на уровне типов: он позволяет получить **тип переменной или функции**.

// JavaScript typeof (runtime):
typeof 'hello'   // 'string'
typeof 42        // 'number'

// TypeScript typeof (на уровне типов):
const config = {
  host: 'localhost',
  port: 3000,
  debug: true,
}

type Config = typeof config
// type Config = {
//   host: string
//   port: number
//   debug: boolean
// }

// Тип функции:
function add(a: number, b: number): number {
  return a + b
}

type AddFn = typeof add
// type AddFn = (a: number, b: number) => number

Зачем нужен typeof на уровне типов

Основной сценарий: у тебя есть значение (константа, объект, функция), и ты хочешь использовать его тип **без повторного написания**:

// Без typeof — дублирование:
interface Config {
  host: string
  port: number
}
const config: Config = { host: 'localhost', port: 3000 }

// С typeof — тип выводится из значения:
const config = { host: 'localhost', port: 3000 }
type Config = typeof config  // Автоматически: { host: string; port: number }

// Особенно полезно с as const:
const COLORS = { RED: 'red', GREEN: 'green', BLUE: 'blue' } as const
type Colors = typeof COLORS
// type Colors = { readonly RED: 'red'; readonly GREEN: 'green'; readonly BLUE: 'blue' }

keyof — получение ключей типа

keyof T возвращает **объединение** (union) строковых литеральных типов всех ключей типа T:

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

type UserKeys = keyof User
// type UserKeys = 'id' | 'name' | 'email'

// Использование keyof для строгой типизации:
function getField(user: User, field: keyof User): string | number {
  return user[field]
}

getField(user, 'name')   // OK
getField(user, 'id')     // OK
getField(user, 'phone')  // Ошибка TS: 'phone' не в keyof User

Комбинация typeof + keyof

Вместе они позволяют работать с ключами **значений** (не только интерфейсов):

const defaultSettings = {
  theme: 'dark',
  language: 'ru',
  fontSize: 14,
  notifications: true,
}

type SettingsKey = keyof typeof defaultSettings
// type SettingsKey = 'theme' | 'language' | 'fontSize' | 'notifications'

function updateSetting(
  settings: typeof defaultSettings,
  key: keyof typeof defaultSettings,
  value: typeof defaultSettings[keyof typeof defaultSettings]
) {
  return { ...settings, [key]: value }
}

ReturnType и другие утилиты используют typeof

function createUser(name: string, age: number) {
  return { id: Math.random(), name, age, createdAt: new Date() }
}

// Тип возвращаемого значения функции:
type User = ReturnType<typeof createUser>
// type User = { id: number; name: string; age: number; createdAt: Date }

// Параметры функции:
type CreateUserParams = Parameters<typeof createUser>
// type CreateUserParams = [name: string, age: number]

Итог: когда применять

  • **typeof** (type-level): когда нужно получить тип существующего значения
  • **keyof**: когда нужно получить union всех ключей типа
  • **keyof typeof**: когда нужно получить union всех ключей существующего объекта
  • Примеры

    typeof и keyof паттерны в JavaScript: работа с ключами объектов и типобезопасный доступ

    // В TS: typeof obj даёт тип объекта, keyof T даёт union ключей
    // В JS: Object.keys() — runtime аналог keyof typeof obj
    
    const defaultSettings = {
      theme: 'dark',
      language: 'ru',
      fontSize: 14,
      notifications: true,
    }
    
    // === keyof typeof: получение ключей объекта ===
    console.log('=== Ключи объекта (keyof typeof) ===')
    
    // В TS: type SettingsKey = keyof typeof defaultSettings = 'theme' | 'language' | ...
    const settingsKeys = Object.keys(defaultSettings)
    console.log('Допустимые ключи:', settingsKeys)
    // ['theme', 'language', 'fontSize', 'notifications']
    
    // === Типобезопасный getProperty (typeof + keyof) ===
    function getProperty(obj, key) {
      // В TS: function getProperty<T>(obj: T, key: keyof T): T[keyof T]
      const validKeys = Object.keys(obj)
      if (!validKeys.includes(key)) {
        throw new Error(`Недопустимый ключ: '${key}'. Допустимые: ${validKeys.join(', ')}`)
      }
      return obj[key]
    }
    
    console.log('\n=== getProperty ===')
    console.log(getProperty(defaultSettings, 'theme'))     // 'dark'
    console.log(getProperty(defaultSettings, 'fontSize'))  // 14
    
    try {
      getProperty(defaultSettings, 'color')  // В TS: ошибка компиляции через keyof
    } catch (e) {
      console.log(e.message)  // 'Недопустимый ключ: color. ...'
    }
    
    // === updateSetting: typeof + keyof для patch объекта ===
    function updateSetting(settings, key, value) {
      const validKeys = Object.keys(settings)
      if (!validKeys.includes(key)) {
        throw new Error(`Ключ '${key}' не существует в настройках`)
      }
      return { ...settings, [key]: value }
    }
    
    console.log('\n=== updateSetting ===')
    const updated1 = updateSetting(defaultSettings, 'theme', 'light')
    console.log(updated1.theme)     // 'light'
    console.log(updated1.language)  // 'ru' — остальные поля не изменились
    
    const updated2 = updateSetting(defaultSettings, 'fontSize', 16)
    console.log(updated2.fontSize)  // 16
    
    // === ReturnType паттерн: тип из функции ===
    console.log('\n=== typeof function (ReturnType паттерн) ===')
    
    function createUser(name, age) {
      // В TS: ReturnType<typeof createUser> даёт тип этого объекта
      return { id: Math.floor(Math.random() * 1000), name, age, createdAt: new Date().toISOString() }
    }
    
    const user = createUser('Алексей', 30)
    console.log(user)  // { id: ..., name: 'Алексей', age: 30, createdAt: '...' }
    
    // Проверяем ключи созданного объекта:
    const userKeys = Object.keys(user)
    console.log('Ключи пользователя:', userKeys)  // ['id', 'name', 'age', 'createdAt']
    
    // === Перебор всех ключей с типами значений ===
    console.log('\n=== Перебор ключей и типов ===')
    
    Object.entries(defaultSettings).forEach(([key, value]) => {
      // В TS: key: keyof typeof defaultSettings, value: typeof defaultSettings[key]
      console.log(`${key}: ${typeof value} = ${value}`)
    })