valueof в TypeScript: как получить тип значения из объекта

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Is there a `valueof` similar to `keyof` in TypeScript? Оптимизация и производительность

Вы когда-нибудь писали на TypeScript и ловили себя на мысли: «У меня есть keyof для ключей, а где же valueof для значений?» Если да — вы не одиноки. Этот вопрос на Stack Overflow собрал тысячи просмотров, и сегодня мы разберём его от и до. Но не просто как абстрактную теорию, а с фокусом на оптимизацию и производительность вашего кода. Погнали!

Почему `keyof` есть, а `valueof` — нет?

В TypeScript есть встроенный утилитарный тип keyof. Он берёт объектный тип и возвращает объединение всех его ключей:

type Person = {
  name: string;
  age: number;
  isActive: boolean;
};

type PersonKeys = keyof Person; // "name" | "age" | "isActive"

А что насчёт значений? Хочется получить string | number | boolean из того же типа. В стандартной библиотеке TypeScript такого нет. Почему? Потому что valueof — это не просто «обратная операция». У объекта может быть много ключей с одинаковыми типами, и нам нужно решить, как это объединять. Но это не значит, что мы не можем написать свой valueof!

Создаём свой `ValueOf`

Самый простой способ — использовать индексный тип доступа (indexed access type) в сочетании с keyof:

type ValueOf<T> = T[keyof T];

type PersonValues = ValueOf<Person>; // string | number | boolean

Вот и всё! Мы только что создали дженерик-тип ValueOf, который работает как valueof. Но давайте копнём глубже — как это влияет на производительность и как использовать с умом.

Оптимизация: когда `ValueOf` спасает от дублирования

Представьте, что у вас есть конфигурационный объект с десятками полей. Вы хотите написать функцию, которая принимает любое значение из этого объекта. Без ValueOf вы бы перечисляли типы вручную:

type Config = {
  apiUrl: string;
  timeout: number;
  retries: number;
  debug: boolean;
};

// Плохо: дублирование типов
function setConfigValue(key: string, value: string | number | boolean) {
  // ...
}

А теперь с ValueOf:

// Хорошо: автоматически подхватывает все типы значений
function setConfigValue(key: keyof Config, value: ValueOf<Config>) {
  // TypeScript знает, что value может быть только string | number | boolean
}

Это не только сокращает код, но и делает его самодокументируемым. Если вы добавите новое поле в Config, TypeScript подхватит новый тип автоматически. Меньше ошибок — больше производительности разработки.

Продвинутый `ValueOf` с условными типами

Иногда нужно получить значения только определённых ключей. Например, все числовые значения. Тут на помощь приходят условные типы (conditional types):

type NumericValues<T> = {
  [K in keyof T]: T[K] extends number ? T[K] : never;
}[keyof T];

type ConfigNumeric = NumericValues<Config>; // number (timeout | retries)

Этот приём особенно полезен при работе с большими типами: вы можете отфильтровать только нужные значения и избежать лишних проверок в рантайме. Оптимизация на уровне типов — это бесплатная производительность, потому что типы исчезают при компиляции.

Производительность в рантайме: а нужно ли?

Важный момент: ValueOf — это чисто типовая конструкция. Она не генерирует никакого JavaScript-кода. Но её правильное использование может косвенно повысить производительность вашего приложения:

  • Меньше багов — меньше времени на отладку.
  • Более строгая типизация — компилятор ловит ошибки до рантайма.
  • Легче рефакторить — изменения в одном месте не ломают всё.

Однако есть и подводные камни. Если вы используете ValueOf с очень большими объединениями типов (например, объект с 100+ полями), компилятор может замедлиться. Но это случается редко и обычно решается разбиением типов на более мелкие.

Реальный кейс: валидация данных

Допустим, вы пишете валидатор для формы. У вас есть тип полей и их допустимых значений. С ValueOf вы можете создать универсальную функцию:

type FormFields = {
  email: string;
  age: number;
  subscribe: boolean;
};

function validateField<K extends keyof FormFields>(
  field: K,
  value: ValueOf<Pick<FormFields, K>>
): boolean {
  // TypeScript знает, что value — это именно тот тип, который соответствует ключу
  switch (field) {
    case 'email':
      return typeof value === 'string' && value.includes('@');
    case 'age':
      return typeof value === 'number' && value > 0;
    case 'subscribe':
      return typeof value === 'boolean';
    default:
      return false;
  }
}

// Использование
validateField('email', 'test@example.com'); // OK
validateField('age', 25); // OK
validateField('email', 123); // Ошибка на этапе компиляции!

Обратите внимание на Pick<FormFields, K> — мы сужаем тип до одного поля, чтобы ValueOf вернул точный тип, а не объединение всех. Это ключевая оптимизация, которая делает код и безопасным, и производительным.

Сравнение с другими подходами

Иногда разработчики пытаются имитировать valueof через перечисляемые типы (enums) или константы. Сравним:

Подход Плюсы Минусы
Enum Простота, рантайм-значения Не гибкий, сложно расширять
ValueOf (наш) Гибкость, автоматическое обновление

Блоги

Рекомендаций по книгам