Avatar ·

Аналог `valueof` для `keyof` в TypeScript: как получить тип значений объекта?

Всем привет! Я недавно начал изучать TypeScript и столкнулся с интересной проблемой. Мы все знаем про `keyof` — он отлично работает, когда нужно получить объединение ключей объекта. Например: ```typescript interface User { name: string; age: number; email: string; } type UserKeys = keyof User; // "name" | "age" | "email" ``` Но мне нужно нечто обратное — получить объединение *типов значений* объекта. То есть для интерфейса `User` я хочу получить тип `string | number`, а не `"name" | "age" | "email"`. Я пробовал писать что-то вроде `valueof`, но TypeScript выдаёт ошибку. Погуглил — на Stack Overflow нашёл похожий вопрос, но ответы там на английском, и я не совсем разобрался. Конкретная задача: у меня есть конфигурационный объект с разными типами полей (строки, числа, булевы значения). Мне нужно написать функцию, которая принимает любой ключ из этого объекта и возвращает значение правильного типа. Без `valueof` приходится везде использовать `any`, что убивает всю типобезопасность. Подскажите, есть ли в TypeScript встроенный аналог `keyof` для значений? Или как правильно реализовать такую утилиту самостоятельно? ```typescript // Пример того, что я хочу const config = { host: 'localhost', port: 3000, debug: true } as const; // Хочу получить тип: string | number | boolean type ConfigValues = ???; // Как это написать? ``` Заранее спасибо за помощь! <<>>

Решение: утилитарный тип `ValueOf`

Да, в TypeScript нет встроенного аналога keyof для значений, но его очень легко создать самостоятельно. Основная идея — использовать индексированный доступ к типу через T[keyof T]. Это выражение означает: «возьми все возможные значения свойств объекта T и объедини их в union».

Вот как выглядит базовая реализация:

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

Теперь применим её к вашему примеру:

const config = {
  host: 'localhost',
  port: 3000,
  debug: true
} as const;

type ConfigValues = ValueOf<typeof config>;
// Результат: string | number | boolean

Обратите внимание на as const. Без него TypeScript вывел бы более широкие типы (string, number, boolean), а с as const — конкретные литералы ('localhost', 3000, true). Но ValueOf работает в обоих случаях.

Расширенная версия для вложенных объектов

Если вам нужно получать типы значений из вложенных структур, можно сделать рекурсивную версию:

type DeepValueOf<T> = T extends object
  ? { [K in keyof T]: DeepValueOf<T[K]> }[keyof T]
  : T;

interface NestedConfig {
  server: {
    host: string;
    port: number;
  };
  features: {
    darkMode: boolean;
    analytics: boolean;
  };
}

type AllValues = DeepValueOf<NestedConfig>;
// Результат: string | number | boolean

Практический пример с функцией

Вернёмся к вашей задаче с функцией, которая принимает ключ и возвращает значение правильного типа:

function getConfigValue<K extends keyof typeof config>(key: K): ValueOf<typeof config> {
  return config[key];
}

// Использование
const host = getConfigValue('host'); // тип: string | number | boolean

Обратите внимание: возвращаемый тип всё ещё string | number | boolean, а не конкретный тип для конкретного ключа. Чтобы сделать более точную типизацию, используйте индексированный доступ напрямую:

function getConfigValue<K extends keyof typeof config>(key: K): typeof config[K] {
  return config[key];
}

const host = getConfigValue('host'); // тип: "localhost"
const port = getConfigValue('port'); // тип: 3000

Важные замечания

  • Производительность: ValueOf не влияет на рантайм — это чисто компиляторная конструкция.
  • Ограничения: Если объект содержит методы (функции), они тоже попадут в union. Чтобы исключить их, можно добавить фильтрацию:
type ValueOfNonFunction<T> = {
  [K in keyof T]: T[K] extends Function ? never : T[K];
}[keyof T];

Этот подход полностью покрывает потребности, для которых мог бы быть нужен гипотетический valueof. Используйте его в своих проектах для повышения типобезопасности!

<<>>

Альтернативный подход: использование условных типов

Если вам нужно получать значения только определённых типов (например, только строки или только числа), можно комбинировать ValueOf с условными типами:

type ValueOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? T[K] : never;
}[keyof T];

interface Mixed {
  id: number;
  name: string;
  active: boolean;
  price: number;
}

type StringValues = ValueOfType<Mixed, string>; // string
type NumberValues = ValueOfType<Mixed, number>; // number

Это полезно, когда нужно работать только с подмножеством значений объекта. Например, для валидации или фильтрации данных.

Для маппированных типов

Ещё один вариант — использовать infer с условными типами, если вам нужно получить тип значения по конкретному ключу:

type ValueForKey<T, K> = K extends keyof T ? T[K] : never;

type NameType = ValueForKey<Mixed, 'name'>; // string

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