TypeScript / 39 минут

keyof и индексный доступ

Свяжите ключ объекта с типом его значения без потери информации.

В процессе Теория: впереди Практика: впереди Тест: впереди
07 / 12
После этой главы
  • Свяжите ключ объекта с типом его значения без потери информации.
  • Применить тему в задаче «keyof и индексный доступ» и объяснить граничные случаи.

Около 9 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Ключи как часть статического контракта

Когда функция читает поле объекта по переданному имени, хочется связать допустимые имена с реальной формой объекта. Произвольный string слишком широк: он разрешает ключ, которого в модели нет. keyof T извлекает союз ключей типа T и позволяет выразить такую связь. Это операция над типами, она не перечисляет runtime-свойства объекта автоматически.

Для формы {name:string,age:number} результат keyof — 'name' | 'age'. Это не string | number из типов значений. Полезно каждый раз проговаривать, на какой вопрос отвечает выражение: «какие имена разрешены» или «какие значения лежат по этим именам». Смешение этих двух уровней часто приводит к ненужным any и неверным утверждениям.

TypeScript
type User = { name: string; age: number };
type UserKey = keyof User;
const key: UserKey = 'name';
// const invalid: UserKey = 'password'; // ошибка

T[K] и точный результат чтения

Индексный доступ к типу T[K] извлекает тип значения по ключу. Если K является союзом нескольких ключей, результатом может стать соответствующий союз типов значений. Он не выполняет runtime-доступ сам по себе. Реальный object[key] должен присутствовать в функции отдельно и соответствовать объявленной связи.

TypeScript
type Name = User['name']; // string
type PublicValue = User[keyof User]; // string | number
function property<T, K extends keyof T>(object: T, key: K): T[K] {
  return object[key];
}

Ограничение K extends keyof T запрещает произвольное имя. Возврат T[K] сохраняет связь конкретного ключа с результатом. Если заменить результат на any, вы потеряете главную пользу обобщения. Если всегда возвращать string | number, обобщённая функция перестанет подходить объектам с другими видами значений.

typeof в позиции типа

В TypeScript typeof может использоваться для получения статического типа уже объявленного значения. Это помогает избежать дублирования конфигурации: сначала создаётся runtime-объект, затем из его типа выводят ключи и значения. Не путайте такую запись с JavaScript-проверкой typeof value, которая выполняется в программе и возвращает строку.

TypeScript
const routes = { home: '/', courses: '/courses' } as const;
type RouteName = keyof typeof routes;
type RoutePath = typeof routes[RouteName];

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

Почему Object.keys возвращает не идеальный keyof[]

Структурный тип не всегда описывает полный закрытый набор runtime-свойств. Объект, переданный как User, может фактически иметь дополнительные поля. Поэтому безусловное обещание, что Object.keys(value) перечислит только keyof User, могло бы быть неверным. Статическая совместимость формы и точное перечисление реального объекта — разные гарантии.

TypeScript
const detailed = { name: 'Анна', age: 20, internal: true };
const user: User = detailed;
console.log(Object.keys(user)); // internal тоже существует в runtime

Не решайте каждый такой случай утверждением as (keyof T)[] без анализа. Иногда нужен явный список разрешённых ключей, иногда собственная проверка схемы, иногда более подходящий контейнер Map. Утверждение типа допустимо лишь там, где у вас есть независимое основание для заявленной гарантии, а не просто желание убрать сообщение компилятора.

Динамические ключи и необязательные значения

Если тип имеет индексную сигнатуру, keyof может включать более широкий набор ключей. При optional-поле чтение T[K] также учитывает возможное отсутствие. Это честное описание: допустимость имени не доказывает наличие необязательного свойства у конкретного объекта. Для обычного словаря произвольных ключей полезно рассмотреть noUncheckedIndexedAccess и явный результат отсутствия.

TypeScript
type Settings = { title?: string; limit: number };
type Title = Settings['title']; // string | undefined
function title(value: Settings): string {
  return value.title ?? 'Без названия';
}

Разделяйте проверку разрешённого имени и проверку значения. Например, пользователь может выбрать разрешённый столбец, но конкретная запись не иметь optional-данных для него. Интерфейс должен согласованно показать отсутствие, а не случайно преобразовать undefined в видимую подпись.

Практическое чтение свойства

В задании generic helper get связывает T, K и T[K], а solve использует его для name или age. Числовой возраст должен остаться числом, включая ноль; пустое имя должно остаться строкой. Преобразование всех результатов в строку нарушит контракт, хотя визуальный вывод в консоли может выглядеть похожим.

Проверьте оба ключа на нескольких объектах. Затем попробуйте передать 'email' и убедитесь, что ошибка возникает при статической проверке. Для самостоятельной тренировки опишите функцию обновления свойства с третьим параметром value:T[K]. Объясните, почему такая сигнатура не позволит записать строку в age, если конкретный K известен как 'age'.

Загружаем редактор…

Музыка: Reverie — Scott Buckley · CC BY 4.0