Avatar ·

Существует ли в TypeScript аналог `keyof` для получения типов значений объекта?

Всем привет! Я только начал осваивать TypeScript и столкнулся с такой ситуацией. Я знаю, что оператор `keyof` позволяет получить объединение ключей объекта. Например, если у меня есть интерфейс `User`, то `keyof User` вернет тип `"id" | "name" | "email"`. Это очень удобно. Но мне нужно сделать обратное: получить тип значений объекта. Допустим, у меня есть конфигурация: ```typescript const appConfig = { apiUrl: 'https://api.example.com', port: 3000, debug: true }; ``` Я хочу создать тип, который будет представлять все возможные значения из этого объекта, то есть `string | number | boolean`. В реальном проекте у меня может быть 20+ полей с разными типами, и перечислять их вручную — это ужасно. Я пробовал что-то вроде `valueof typeof appConfig`, но TypeScript выдает ошибку. В документации я нашел только `keyof`. Может быть, есть какой-то встроенный механизм, который я пропустил? Или нужно писать свою утилиту? Буду благодарен за примеры с кодом. Заранее спасибо за помощь! <<>>

Прямого аналога нет, но есть простое решение

В TypeScript действительно нет встроенного оператора valueof, работающего как keyof. Однако вы можете легко создать свой условный тип, который будет извлекать все типы значений из объекта. Для этого используется синтаксис доступа по индексу (indexed access type) в сочетании с keyof.

Основной подход: T[keyof T]

Самый простой способ получить объединение значений объекта — написать Т[keyof T]. Это работает для любых типов, включая интерфейсы, типы и объекты, обернутые в typeof.

// Для вашего примера с конфигом
const appConfig = {
    apiUrl: 'https://api.example.com',
    port: 3000,
    debug: true
};

// Создаем тип значения
type AppConfigValues = typeof appConfig[keyof typeof appConfig];
// Результат: string | number | boolean

// Пример использования функции, принимающей любое значение из конфига
function logConfigValue(value: AppConfigValues) {
    console.log(value);
}

logConfigValue(appConfig.apiUrl);  // OK
logConfigValue(appConfig.port);    // OK
logConfigValue(appConfig.debug);   // OK
// logConfigValue('custom');       // Ошибка, если нужно строго из конфига

Создание переиспользуемого типа ValueOf

Для удобства можно объявить generic-тип, который можно будет переиспользовать в проекте:

// Утилита для получения типов значений
type ValueOf<T> = T[keyof T];

// Теперь можно писать более читаемо
type AppConfigValues = ValueOf<typeof appConfig>;  // string | number | boolean

// Работает с интерфейсами
interface User {
    id: number;
    name: string;
    role: 'admin' | 'user';
}
type UserValues = ValueOf<User>;  // number | string | 'admin' | 'user'

Важные нюансы и продвинутые варианты

  • Работа с массивами: Для массивов T[keyof T] вернет методы массива (length, push, pop и т.д.), а не элементы. Для получения типа элементов используйте T[number].
  • Фильтрация по типу ключа: Если нужно получить значения только для ключей определенного типа (например, только строковые значения), используйте условные типы:
    type StringValues<T> = {
        [K in keyof T]: T[K] extends string ? T[K] : never
    }[keyof T];
    
    type ConfigStringValues = StringValues<typeof appConfig>;  // только 'https://api.example.com'
  • Глубокое извлечение: Для вложенных объектов можно использовать рекурсивные типы, но это выходит за рамки базового вопроса.

Почему нет встроенного valueof?

Разработчики TypeScript решили не добавлять отдельный оператор, потому что T[keyof T] — это идиоматическое и гибкое решение. Оно легко комбинируется с другими утилитами (Pick, Omit, Partial) и не требует новых синтаксических конструкций. К тому же, keyof возвращает тип, а valueof в некоторых языках (например, в Java) работает на уровне значений, что для TypeScript было бы неоднозначно.

Таким образом, используйте T[keyof T] или создайте тип-обертку ValueOf<T> — это стандартный и рекомендуемый способ в TypeScript сообществе.

<<>>

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

Если вам нужно не просто получить все значения, а, например, исключить определенные типы (null, undefined) или работать с конкретными ключами, можно использовать более точные конструкции.

Пример: получение только не-null значений

type NonNullableValues<T> = {
    [K in keyof T]: T[K] extends null | undefined ? never : T[K]
}[keyof T];

const data = {
    a: 'hello',
    b: null,
    c: 42,
    d: undefined
};

type CleanValues = NonNullableValues<typeof data>;  // string | number

Пример: значения только для определенных ключей

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

// Только значения для строковых ключей
type StringKeyValues<T> = {
    [K in keyof T as K extends string ? K : never]: T[K]
}[keyof T];

// Или с использованием Pick
type PickedValues<T, K extends keyof T> = T[K];

// Пример:
type UserSubset = PickedValues<User, 'id' | 'name'>;  // number | string

Этот подход особенно полезен, когда вы работаете с большими объектами и хотите ограничить область видимости типа. Хотя T[keyof T] — самое простое решение, условные типы дают больше контроля и гибкости в сложных сценариях.