Введение: Зачем вам нужны ключи интерфейса в виде массива?
Представьте: вы работаете над крупным проектом на TypeScript, и вам нужно пройтись по всем полям сложного объекта, сформировать динамическую форму на React, отфильтровать данные или просто вывести список доступных свойств. Проблема в том, что TypeScript — это надстройка над JavaScript, и его типы существуют только во время компиляции. В рантайме они исчезают. Как же быть? Как получить ключи TypeScript интерфейса в виде массива строк, чтобы использовать их в реальном коде?
Вопрос, который мучает тысячи разработчиков на Stack Overflow. Сегодня, в 2025 году, мы разберем все возможные подходы: от простых и надежных до продвинутых. Вы узнаете, как заставить TypeScript работать на вас, а не против вас. Поехали!
Проблема: "Исчезающие" типы
Давайте сразу посмотрим на корень проблемы. Допустим, у вас есть такой интерфейс:
interface User {
id: number;
name: string;
email: string;
isAdmin: boolean;
}
Вам нужно получить массив строк: ['id', 'name', 'email', 'isAdmin']. Первое, что приходит в голову — использовать Object.keys(). Но есть нюанс. Если вы попробуете написать так:
const keys = Object.keys({} as User);
// Тип: string[]
TypeScript вернет вам тип string[], а не ('id' | 'name' | 'email' | 'isAdmin')[]. Это бессмысленно — вы теряете всю информацию о конкретных ключах.
Почему так происходит? Потому что Object.keys() в JavaScript всегда возвращает массив строк. TypeScript не может знать наверняка, какие именно ключи будут у объекта во время выполнения (например, если объект расширяется). Поэтому он дает самый безопасный, но бесполезный тип.
Решение 1: "Ручной" массив (Простота и безопасность)
Самый простой и рекомендуемый для 90% случаев способ — создать массив вручную и вывести из него тип. Это не шутка. Такой подход гарантирует, что ваш массив всегда будет синхронизирован с интерфейсом.
// 1. Определяем массив ключей
export const USER_KEYS = ['id', 'name', 'email', 'isAdmin'] as const;
// 2. Выводим тип из массива
type UserKey = typeof USER_KEYS[number]; // 'id' | 'name' | 'email' | 'isAdmin'
// 3. Используем для интерфейса
interface User {
[key in UserKey]: key extends 'id' ? number
: key extends 'name' ? string
: key extends 'email' ? string
: boolean;
}
// 4. Получаем массив строк
const keys: string[] = [...USER_KEYS];
console.log(keys); // ['id', 'name', 'email', 'isAdmin']
Плюсы: Полная типизация, никаких магических трюков, легко читать и поддерживать.
Минусы: Нужно поддерживать два места (интерфейс и массив) вручную.
Решение 2: Утилитарный тип с keyof (Продвинутый, но с оговорками)
TypeScript предоставляет оператор keyof, который возвращает объединение ключей (union type). Но как превратить это объединение в массив строк? В 2025 году это все еще требует небольшого трюка.
Классический способ — использовать вспомогательную функцию, которая проверяет типы во время компиляции:
// Вспомогательная функция
function getKeys<T>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
// Использование
interface Product {
title: string;
price: number;
inStock: boolean;
}
const product: Product = {
title: 'Keyboard',
price: 100,
inStock: true
};
const productKeys = getKeys(product);
// Тип: ("title" | "price" | "inStock")[]
console.log(productKeys); // ['title', 'price', 'inStock']
Важно: Этот метод работает только если у вас есть реальный объект, соответствующий интерфейсу. Если у вас есть только интерфейс, но нет объекта, придется создать заглушку:
const keys = getKeys({} as Product);
// Все еще работает, но нужно быть осторожным с пустыми объектами
Плюсы: Автоматическая синхронизация с интерфейсом, не нужно дублировать код.
Минусы: Требует объект (или заглушку), не работает напрямую с интерфейсом без экземпляра.
Решение 3: Шаблонные литералы и рекурсивные типы (Хардкор)
Для тех, кто любит сложные вызовы, существует возможность создать рекурсивный тип, который "вычисляет" ключи интерфейса во время компиляции. Этот метод не так популярен, но он демонстрирует мощь TypeScript.
// Определяем тип, который собирает ключи
type KeysOf<T> = {
[K in keyof T]: K extends string ? K : never;
}[keyof T];
// Использование
interface Config {
apiUrl: string;
timeout: number;
retries: number;
}
type ConfigKeys = KeysOf<Config>; // "apiUrl" | "timeout" | "retries"
// Чтобы получить массив, нужно использовать const assertion
const configKeysArray: ConfigKeys[] = ['apiUrl', 'timeout', 'retries'];
Этот подход интересен, но на практике он не дает преимущества перед ручным массивом. Вы все равно должны явно перечислить ключи в массиве.
Решение 4: Библиотеки и утилиты (Экосистема 2025)
В 2025 году существует несколько библиотек, которые решают эту проблему. Но стоит ли их использовать?
- ts-essentials: Предоставляет тип
Keys, который делает то же самое, что и нашKeysOf. - type-fest: Одна из самых популярных библиотек. Есть тип
ValueOf, но для ключей придется писать свой велосипед. - zod: Если вы используете Zod для валидации, вы можете получить ключи из схемы:
z.object({...}).keyof().
Рекомендация: Если ваш проект уже использует Zod или другую библиотеку валидации, используйте её. В противном случае не стоит тащить зависимость только ради получения ключей.
Лучшая практика 2025: Гибридный подход
После анализа всех методов, я рекомендую следующий подход для 2025 года:
- Для небольших проектов или прототипов — используйте ручной массив с
as const. Это просто и надежно. - Для крупных проектов с частыми изменениями — используйте функцию
getKeysс приведением типа. Добавьте тесты, чтобы убедиться, что массив соответствует интерфейсу. - Если вы используете валидацию (Zod, Yup) — получайте ключи из схемы. Это самый "чистый" способ, так как схема