Сегодня нас ждет один из тех уроков, который отделяет любителя от профессионала. Мы будем разбираться с двумя мощнейшими операторами типов: keyof и typeof. Не пугайтесь, если названия звучат сложно. Я уверен, что к концу этого урока вы будете использовать их с легкостью и удивляться, как же раньше без них жили.
Эти операторы не просто синтаксический сахар. Это фундаментальные инструменты для создания гибких, переиспользуемых и типобезопасных конструкций. Они позволяют TypeScript глубже понять структуру ваших данных и вывести новые типы на основе уже существующих значений и определений.
Оператор keyof: Получаем ключи в виде Union типа
Давайте начнем с оператора keyof. Его основная и единственная задача взять тип объекта и выдать нам union-тип, состоящий из всех его ключей. Проще говоря, он извлекает все названия свойств объекта и превращает их в тип "ключ1" | "ключ2" | ....
Представьте, что у вас есть интерфейс, описывающий пользователя. keyof поможет вам создать тип, который может быть только одним из имен его свойств: 'name', 'age' или 'email'. Это невероятно полезно, когда вы пишете функции, которые работают со свойствами объектов, например, геттеры, сеттеры или функции сортировки.
Без keyof нам пришлось бы вручную дублировать эти названия, что немедленно привело бы к рассинхронизации типов при изменении исходного интерфейса. keyof же делает эту связь прочной и неразрывной. Он гарантирует, что наш код всегда будет знать о точной структуре объекта, с которым он работает и будет автоматически адаптироваться к любым изменениям в его типе.
Базовый пример:
interface IUser { id: number; name: string; email: string; age: number; } // UserKeysType будет равен типу: "id" | "name" | "email" | "age" type UserKeysType = keyof IUser; // Это работает корректно: const key1: UserKeysType = 'name'; // OK const key2: UserKeysType = 'email'; // OK // Это вызовет ошибку типа: const key3: UserKeysType = 'password'; // Ошибка! Свойства 'password' нет в IUser
В этом примере мы создаем новый тип UserKeysType, который может содержать только строковые значения, совпадающие с именами свойств интерфейса IUser. Компилятор TypeScript не пропустит попытку присвоить этому типу любое другое значение.
Практическое применение: функция-геттер
Одно из самых частых применений keyof, это создание универсальных функций для доступа к свойствам объекта.
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user: IUser = { id: 1, name: 'Maxim', email: 'max@example.com', age: 30 }; // Использование: const userName = getValue(user, 'name'); // Тип userName - string const userAge = getValue(user, 'age'); // Тип userAge - number // Компилятор подсветит ошибку: const userBio = getValue(user, 'bio'); // Ошибка: Аргумент типа '"bio"' не может быть присвоен параметру типа '"id" | "name" | "email" | "age"'.
Давайте разберем эту магию по полочкам:
-
<T, K extends keyof T>: Мы объявляем два дженерика.Tэто тип нашего объекта.Kэто тип, который расширяетkeyof T. То естьKможет быть только одним из ключей типаT. -
(obj: T, key: K): Функция принимает объект типаTи ключ типаK. -
: T[K]: Возвращаемый тип, это тип значения, лежащего в объектеTпо ключуK(так называемый lookup type). ДляK='name'это будетstring, дляK='age'—number.
Эта простая функция теперь абсолютно типобезопасна. Она не позволит обратиться к несуществующему свойству, а возвращаемое значение будет иметь правильный тип. Попробуйте реализовать такое на чистом JavaScript, у вас не получится такой же уровень надежности и автодополнения в IDE.
keyof и Mapped Types (Сопоставленные типы)
Истинная мощь keyof раскрывается в комбинации с Mapped Types. Это продвинутая, но очень полезная концепция, которая позволяет вам создавать новые типы на основе старых, итерируясь по их ключам.
Самый классический пример, это создание типа, который делает все свойства объекта опциональными или только для чтения.
// Встроенные в TS утилиты Readonly и Partial выглядят примерно так: type MyReadonly<T> = { readonly [P in keyof T]: T[P]; // Добавляем 'readonly' каждому свойству }; type MyPartial<T> = { [P in keyof T]?: T[P]; // Добавляем '?' каждому свойству }; // Применяем наш самодельный Readonly к IUser type ReadonlyUser = MyReadonly<IUser>; // Эквивалентно: // interface ReadonlyUser { // readonly id: number; // readonly name: string; // readonly email: string; // readonly age: number; // }
Конструкция [P in keyof T] означает: «для каждого ключа P в union-типе keyof T создай свойство с именем P и типом T[P]«. Это как цикл for, но на уровне типов!
Оператор typeof в контексте типов
Теперь перейдем к оператору typeof. Здесь очень важно не путать его с оператором typeof из JavaScript, который возвращает строку с названием типа значения во время выполнения ("string", "object", "function" и т.д.).
В TypeScript, когда typeof используется в контексте типов, он выполняет иную и крайне полезную функцию: он извлекает статический тип значения, которое было объявлено на уровне переменной. Проще говоря, он позволяет сказать: «Создай тип на основе формы этой переменной».
Это идеальный инструмент для работы с данными, тип которых вы не хотите описывать вручную. Например, вы получили объект из внешней библиотеки, где нет типов или создали сложный конфигурационный объект прямо в коде. typeof мгновенно создаст его точный тип.
Простой пример:
// Допустим, у нас есть объект JavaScript const person = { firstName: 'Alice', lastName: 'Smith', age: 28 }; // Мы можем извлечь его тип! type PersonType = typeof person; // Теперь PersonType - это тип: // { // firstName: string; // lastName: string; // age: number; // } // Мы можем использовать этот тип для аннотации других переменных const anotherPerson: PersonType = { firstName: 'Bob', lastName: 'Brown', age: 35 };
Мы только что создали полноценный тип PersonType, не написав ни одной аннотации вручную! Если мы изменим структуру объекта person, тип PersonType автоматически подстроится под эти изменения при следующей компиляции.
Работа с const assertions
Мощь typeof многократно усиливается при использовании с as const. as const говорит TypeScript рассматривать объект или массив как неизменяемый (read-only) и использовать максимально узкие литеральные типы для его значений.
const colors = { red: '#FF0000', green: '#00FF00', blue: '#0000FF', } as const; // Обратите внимание на 'as const' type ColorsType = typeof colors; // Теперь ColorsType - это тип с точными значениями: // { // readonly red: "#FF0000"; // readonly green: "#00FF00"; // readonly blue: "#0000FF"; // } // Мы также можем извлечь тип ключей или значений: type ColorKeys = keyof ColorsType; // "red" | "green" | "blue" type ColorValues = ColorsType[ColorKeys]; // "#FF0000" | "#00FF00" | "#0000FF"
Без as const тип ColorValues был бы просто string. С as const и typeof мы получаем union из конкретных строковых литералов. Это невероятно полезно для создания типобезопасных перечислений, конфигов и тем в приложениях.
Комбинация keyof и typeof
По отдельности эти операторы мощны, но вместе они творят настоящую магию. Самая частая и полезная комбинация, это keyof typeof. Эта конструкция позволяет сначала получить тип переменной (с помощью typeof), а затем извлечь из этого типа union его ключей (с помощью keyof).
Зачем это нужно? Потому что мы не можем использовать keyof напрямую со значением переменной. keyof работает только с типами. Поэтому мы сначала получаем тип значения, а затем получаем ключи этого типа.
Классический пример: типобезопасное извлечение ключей из объекта-конфигурации
Представьте, что у вас есть объект с настройками, объявленный как const. Вы хотите написать функцию, которая принимает имя одной из этих настроек. Как это сделать?
// Объявляем наш конфиг с помощью 'as const' для фиксации типов значений const appConfig = { theme: 'dark', language: 'ru', apiUrl: 'https://api.myapp.com', version: '1.0.0' } as const; // Мы хотим создать тип, который является unionом ключей этого объекта type ConfigKey = keyof typeof appConfig; // Процесс по шагам: // 1. typeof appConfig -> получаем тип { // readonly theme: "dark"; // readonly language: "ru"; // readonly apiUrl: "https://api.myapp.com"; // readonly version: "1.0.0"; // } // 2. keyof ... -> получаем тип "theme" | "language" | "apiUrl" | "version" function getConfigValue(key: ConfigKey) { return appConfig[key]; } // Используем: const theme = getConfigValue('theme'); // OK, тип const theme: "dark" const lang = getConfigValue('language'); // OK, тип const lang: "ru" // Компилятор не даст ошибиться const value = getConfigValue('port'); // Ошибка: Аргумент типа '"port"' не может быть присвоен параметру типа '"theme" | "language" | "apiUrl" | "version"'.
Обратите внимание, что возвращаемый тип функции getConfigValue будет не просто string, а конкретным литеральным типом, соответствующим значению в объекте! Это высший пилотаж типобезопасности.
Еще один пример: типобезопасный Object.keys
Стандартная функция Object.keys() в TypeScript возвращает тип string[]. Это недостаточно строго, ведь мы знаем, какие ключи на самом деле есть у объекта.
const myObject = { a: 1, b: 2, c: 3 } as const; // Стандартное поведение: const keys1 = Object.keys(myObject); // Тип keys1: string[] // Наша улучшенная типобезопасная версия: function getKeys<T extends object>(obj: T): (keyof T)[] { return Object.keys(obj) as (keyof T)[]; } const keys2 = getKeys(myObject); // Тип keys2: ("a" | "b" | "c")[]
Здесь мы используем приведение типа as (keyof T)[], чтобы сообщить компилятору, что мы уверены массив строк, возвращаемый Object.keys, на самом деле является массивом, содержащим только ключи объекта T. Это делает последующую работу с массивом keys2 гораздо более безопасной и удобной.
Практические задачи для закрепления
Давайте напишем немного кода, чтобы закрепить эти знания.
Задача 1: Типобезопасный пропс-валидатор
Представьте, что вы пишете библиотеку компонентов. У компонента Button есть пропсы size, theme и disabled. Создайте объект defaultProps со значениями по умолчанию, а затем используйте typeof и keyof, чтобы создать тип для пропсов компонента, который гарантирует, что пропсы всегда будут соответствовать значениям из defaultProps.
// 1. Объявите объект defaultProps с as const const defaultProps = { size: 'medium', theme: 'primary', disabled: false } as const; // 2. Создайте тип Props на основе defaultProps. // Подсказка: вам нужно сделать свойства опциональными, но чтобы их типы совпадали. type Props = Partial<typeof defaultProps>; // Не совсем верно, т.к. Partial сделает и значения опциональными. // Правильнее: мы хотим, чтобы ключи были из defaultProps, а типы значений - такими же. type Props = { [K in keyof typeof defaultProps]?: (typeof defaultProps)[K]; }; // 3. Напишите функцию createButton, которая принимает объект пропсов типа Props. function createButton(props: Props) { // ... логика компонента return { ...defaultProps, ...props }; } // 4. Протестируйте свою функцию const button = createButton({ size: 'large' }); // OK const badButton = createButton({ size: 'huge' }); // Ошибка: '"huge"' is not assignable to '"small" | "medium" | "large"'
Задача 2: Функция группировки массива объектов
Напишите универсальную функцию groupBy, которая принимает массив объектов и имя ключа, по которому нужно сгруппировать. Используйте keyof и дженерики, чтобы функция была полностью типобезопасной.
interface IUser { id: number; name: string; department: 'engineering' | 'marketing' | 'sales'; } const users: IUser[] = [ { id: 1, name: 'Alice', department: 'engineering' }, { id: 2, name: 'Bob', department: 'marketing' }, { id: 3, name: Charlie', department: 'engineering' }, ]; // Ваша реализация функции groupBy function groupBy<T, K extends keyof T>(array: T[], key: K): Record<string, T[]> { // Приведение типа необходимо, так как key может быть string | number | symbol, // а нам нужно использовать его как строку для объекта. const keyString = key as string; return array.reduce((acc, obj) => { // Извлекаем значение по ключу и преобразуем его в строку const groupName = String(obj[key]); if (!acc[groupName]) { acc[groupName] = []; } acc[groupName].push(obj); return acc; }, {} as Record<string, T[]>); } // Тестирование const groupedByDepartment = groupBy(users, 'department'); /* groupedByDepartment имеет тип: Record<string, IUser[]> и значение: { engineering: [ { id: 1, name: 'Alice', department: 'engineering' }, { id: 3, name: 'Charlie', department: 'engineering' } ], marketing: [ { id: 2, name: 'Bob', department: 'marketing' } ], sales: [] // Представим, что есть и такие } */ // Это вызовет ошибку на этапе компиляции: const groupedByInvalidKey = groupBy(users, 'salary'); // Ошибка: Аргумент типа '"salary"' не может быть присвоен параметру типа '"id" | "name" | "department"'.
Задача 3: Динамическое создание Action Creators для Redux
В мире Redux есть понятие «Action Creator». Эта функция, которая создает объект действия (action). Часто их создают много и их сигнатуры повторяются. Используя typeof и keyof, можно автоматизировать их создание для объекта с набором действий.
// 1. Определяем объект с типами действий (actions) const actionTypes = { ADD_TODO: 'ADD_TODO', REMOVE_TODO: 'REMOVE_TODO', TOGGLE_TODO: 'TOGGLE_TODO', } as const; // 2. Создаем тип для всех возможных действий (Action objects). // Предположим, что все action'ы имеют поле payload. type ActionMap = { [actionType in keyof typeof actionTypes]: { type: typeof actionTypes[actionType]; payload: any }; // В реальности payload у каждого action свой }; // 3. Создаем функцию-генератор action creators function createActionCreators<T extends Record<string, string>>(types: T) { const result: any = {}; for (const key in types) { // Для каждого ключа в объекте types создаем функцию result[key] = (payload: any) => ({ type: types[key], payload }); } return result as { [K in keyof T]: (payload: any) => { type: T[K]; payload: any } }; } // 4. Генерируем все action creators export const todoActions = createActionCreators(actionTypes); // 5. Используем их с полной типобезопасностью! const addTodoAction = todoActions.ADD_TODO('Написать урок'); // addTodoAction имеет тип: { type: "ADD_TODO"; payload: any; } // В более продвинутой версии мы бы типизировали и payload.
Заключение
Операторы keyof и typeof это фундаментальные кирпичики для построения сложных, адаптивных и абсолютно типобезопасных абстракций.
Они позволяют вашему коду «думать» вместе с вами: выводить типы из данных, создавать зависимости между типами и значениями, предотвращать целые классы ошибок еще на этапе компиляции. Комбинация keyof typeof, это ваш пропуск в мир продвинутой типизации, где вы можете работать с объектами JavaScript так, как будто они были описаны самыми подробными интерфейсами.
В следующих урокпх мы продолжим изучать систему типов и поговорим о еще более удивительных вещах.
Это был 22-й урок из моего полного курса по TypeScript для начинающих. Если вы хотите освоить язык с нуля и до уверенного уровня, посмотрите на весь курс: Курс по TypeScript для начинающих
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


