keyof и типы доступа по индексу
keyof даёт объединение ключей типа; T[K] индексирует тип. Индексируй объединением ключей — получишь объединение типов значений, а Arr[number] — тип элемента массива.
Вы определяете интерфейс User, затем вручную пишете второй тип UserKey = "id" | "name" | "email" для функции, выбирающей поле. Коллега добавляет role в User и выкатывает. Спустя три месяца — баг в рантайме: форма конфигурации позволяет редактировать role, но хелпер валидации, завязанный на устаревший UserKey, молча его пропускает. Два определения разошлись, потому что были написаны дважды. Типы доступа по индексу удаляют дубликат: keyof User и есть объединение ключей, выведенное автоматически, а User[K] и есть тип значения. Определите данные один раз; пусть типы следуют за ними.
keyof: объединение ключей типа
keyof T порождает объединение литеральных типов ключей свойств T:
type User = { id: string; name: string; age: number };
type UserKey = keyof User;
// ^? type UserKey = "id" | "name" | "age"Это объединение выводится — добавьте свойство в User, и UserKey обновится автоматически. Это строительный блок для любой операции, которая должна итерировать или ограничивать по собственным ключам типа.
T[K]: доступ по индексу
T[K] находит тип, хранящийся по ключу K в T. Ключ должен быть валидным ключом T (иначе будет ошибка):
type Name = User["name"];
// ^? type Name = string
type Age = User["age"];
// ^? type Age = number
type Bad = User["nope"];
// Error: Property 'nope' does not exist on type 'User'.Индексация идёт по типу, а не по значению: User["name"] использует строковый литеральный тип "name" как индекс, а не рантайм-строку. Поэтому это работает на типах, не имеющих рантайм-существования.
Индексация объединением ключей → объединение типов значений
Когда нужно выразить «значение по любому из этих ключей», не пишите отдельное объединение вручную — проиндексируйте по объединению ключей и дайте проверяющему вывести его. Здесь keyof и T[K] соединяются в настоящий инструмент единого источника истины.
Индексируйте объединением ключей — и получите объединение соответствующих типов значений:
type IdOrAge = User["id" | "age"];
// ^? type IdOrAge = string | number
type AllValues = User[keyof User];
// ^? type AllValues = string | number ("id"→string, "name"→string, "age"→number)T[keyof T] — «индексировать по каждому ключу» — это идиома для «объединения всех типов значений в T». Дублирующиеся типы значений схлопываются (string | string | number = string | number). Это частый сеньорский инструмент: вывести объединение допустимых значений из объекта конфигурации или объединение типов payload из членов размеченного объединения.
type Action =
| { type: "add"; n: number }
| { type: "clear" };
type ActionType = Action["type"];
// ^? type ActionType = "add" | "clear" — объединение тегов, выведенное из вариантовЗаметьте: это работает прямо на объединённом типе: Action["type"] читает type из каждого члена и объединяет результаты. Это держит теги-дискриминанты из прошлого урока как единый источник истины.
Индексация массивов и кортежей через number → тип элемента
Массивы и кортежи индексируются number. Поэтому T[number] — это тип элемента:
type Names = string[];
type Element = Names[number];
// ^? type Element = string
const roles = ["admin", "editor", "viewer"] as const;
type Role = typeof roles[number];
// ^? type Role = "admin" | "editor" | "viewer"Связка as const + [number] — канонический способ вывести литеральное объединение из рантайм-массива, чтобы массив значений и его тип шли в ногу. Для кортежей [number] даёт объединение всех позиций, а литеральный индекс — один слот:
type Pair = [string, number];
type First = Pair[0];
// ^? type First = string
type Either = Pair[number];
// ^? type Either = string | numberkeyof — как спросить у формы, какие у неё поля; T[K] — как спросить, какое значение хранит поле K. Выведение типов так держит один ___ истины вместо двух расходящихся определений.
Подножка: keyof на типах с индексной сигнатурой
Когда у типа есть индексная сигнатура, keyof возвращает тип ключа сигнатуры, а не конечное литеральное объединение:
type Dict = { [key: string]: number };
type K1 = keyof Dict;
// ^? type K1 = string | number
// (строковые индексные сигнатуры допускают и числовые ключи, ведь JS приводит числовые ключи к строкам)
type NumDict = { [key: number]: boolean };
type K2 = keyof NumDict;
// ^? type K2 = numberЭто удивляет: keyof типа со строковым индексом — это string | number, а не string, ведь числовой доступ к свойству вроде dict[0] легален (JS превращает его в dict["0"]). Если позже вы попытаетесь ограничить дженерик K extends keyof Dict, ожидая точные ключи, вы вместо этого получите широкий string | number и потеряете желаемую точность. Предпочитайте конкретный объектный тип (или Record<SpecificUnion, V>), когда нужно конечное объединение ключей.
▸Граничные случаи
Индексация литералом против объединения резко меняет точность. User["id"] — единственный тип значения (string). User[keyof User] — всё объединение типов значений. А User[string] — ошибка, если у User нет строковой индексной сигнатуры: индексация требует, чтобы тип индекса был присваиваем ключу цели. Поэтому obj[someStringVariable] часто ошибается на точно типизированном объекте: рантайм-string шире конечного объединения ключей объекта, поэтому доступ несоунден. Сузьте ключ до keyof T заранее.
Почему это мост к маппинг-типам
Доступ по индексу отвечает на «какой тип по этому ключу». Естественный следующий вопрос — «преобразовать каждый ключ» — это маппинг-типы (юнит 04): { [K in keyof T]: ... } итерирует объединение keyof T и использует T[K], чтобы прочитать тип каждого значения. Всё, что вы делаете с маппинг-типами, построено на двух операторах этого урока, поэтому усвоение keyof и T[K] сейчас напрямую окупится там.
Выводи, а не дублируй
1/3Для `type T = { a: string; b: number; c: string }` — что такое `T[keyof T]`?
У вас `const perms = ['read','write'] as const`. Какой тип даёт объединение `'read' | 'write'`?
- 01Что порождают keyof T и T[K] и почему индексация идёт «по типу, а не по значению»?
- 02Что даёт T[keyof T] и как доступ по индексу ведёт себя на объединении и на массивах?
- 03Почему keyof на типе с индексной сигнатурой удивляет и когда этого избегать?
Типы доступа по индексу закрывают юнит, превращая машинерию объединений в способ выводить типы из данных. keyof T даёт объединение ключей типа, а T[K] читает тип по ключу; индексация объединением ключей даёт объединение типов значений, поэтому T[keyof T] — это все типы значений, а Action["type"] извлекает объединение тегов дискриминанта прямо из вариантов, смоделированных в прошлом уроке. Массивы индексируются number, делая Arr[number] типом элемента, а typeof arr[number] (с as const) — литеральным объединением, вытянутым из рантайм-массива — противоядие от написанных вручную, склонных к расхождению дубликатов. Единственная подножка для запоминания: keyof на строковой индексной сигнатуре — это string | number, а не string. Эти два оператора — keyof и T[K] — ровно те примитивы, что маппинг-типы из юнита 04 используют, чтобы преобразовать каждый ключ типа разом, поэтому навыки выведения, выстроенные здесь, напрямую перейдут в юнит type-system-deep. Теперь, когда видишь объединение ключей, живущее отдельно от своего объекта, тянешься к keyof typeof — а не латаешь дубликат.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.