Индексный доступ T[K]

вывод типов≈ 11 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Тип поля уже написан — внутри типа объекта. Индексный доступ T["поле"] достаёт его оттуда выражением, а не копипастой: скобки те же, что у значений, только слева стоит тип. Та же запись со словом number внутри достаёт тип элемента массива — и именно она превращает список as const в объединение литералов.

Разделы темы:·

Тип поля описан один раз — в типе объекта. Если этот же тип понадобился рядом, его не переписывают руками: берут прямо из объекта квадратными скобками. Синтаксис знакомый по значениям, разница в том, что слева стоит тип, а не переменная, и результат — тоже тип.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Диагностика ровно одна, на предпоследней строке: Тип "гость" не может быть назначен для типа "admin" | "user". Дальше код всё равно выполнился и напечатал «гость» — как и везде в этой части, компилятор высказался, а исполнение пошло своим чередом.

Ценность здесь не в экономии символов, а в связи. Допишите в User третью роль — Role узнает об этом сам. Скопированное объединение type Role = "admin" | "user" не узнает никогда, и однажды разъедется с оригиналом молча.

Единственная синтаксическая ловушка — точка. У значений user.role и user["role"] взаимозаменяемы, у типов работает только вторая форма.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Текст диагностики стоит прочитать целиком, потому что в нём же лежит и ответ: Не удается получить доступ к User.name, так как User является типом, но не является пространством имен. Вы хотели получить тип свойства name в User с использованием User["name"]?

Скобки в типе — это не индексация объекта. Слева тип, внутри тип, снаружи тип. Никакого кода в сборку такая строка не даёт.

Работает это не только с type: интерфейс и класс индексируются точно так же — компилятору важна форма, а не то, каким словом её объявили.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ошибка одна, на label: Тип "number" не может быть назначен для типа "string" — поле класса объявлено без аннотации, и его тип вывелся из значения.

Самая частая ошибка новичка в этом месте — подставить в скобки переменную. Переменная живёт в мире значений, а внутри скобок компилятор ждёт тип, и говорит об этом сразу двумя диагностиками.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Первая — Тип "field" невозможно использовать как тип индекса, вторая — "field" относится к значению, но здесь используется как тип. Возможно, вы имели в виду "typeof field"?. Вторая и есть подсказка: typeof переводит значение в тип, а const дал переменной литеральный тип "name" — то, что и требовалось в скобках.

В скобках необязательно стоять одному литералу. Поставьте объединение — и получите объединение типов всех названных полей.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Обратите внимание на направление: два ключа дали не «поле, которое и число, и строка», а «либо число, либо строка». Индексный доступ отвечает на вопрос «что может лежать по одному из этих ключей», а такой ответ всегда объединение.

Предельный случай — все ключи разом. Их выдаёт keyof из прошлой статьи, и связка T[keyof T] читается как «тип любого значения этого объекта».

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ни одной диагностики: Object.values вернул массив с типом элемента number | string | boolean, а render ровно такой элемент и ждёт. Заметьте, что Cell нигде не перечисляет типы руками — добавится поле в Row, и render начнёт требовать разбора нового варианта.

keyof даёт ключи, индексный доступ — значения. T[keyof T] — это «значения по всем ключам», аналог Object.values на уровне типов.

Если названного поля в типе нет, компилятор говорит об этом теми же словами, что и про обычное свойство. Интереснее случай, когда тип слева — объединение: поле должно найтись у каждого варианта.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Свойство "r" не существует в типе "Shape" — и это правда: у половины значений типа Shape радиуса нет. Поле-метка kind есть у обоих вариантов, поэтому индексный доступ к ней проходит и собирает объединение обеих меток. Это размеченные объединения из главы про сужение.

Можно — но только у типа, который обещал значение для любой строки. Такое обещание даёт индексная сигнатура из прошлой главы; у типа с перечисленными полями его нет.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Диагностика — Тип "User" не содержит соответствующую сигнатуру индекса для типа "string". Это ровно та же граница, что и у TS7053 на значениях: перечисленные поля отвечают за свои ключи, а не за все строки сразу.

У массива нет полей с именами, но ключи у него есть — числовые. Значит, индексный доступ применим и к нему: T[number] означает «тип того, что лежит по любому числовому ключу», то есть тип элемента.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Это самая ходовая форма индексного доступа во всём TypeScript. Список цветов написан один раз и работает дважды: как значение — в COLORS его можно перебрать, вывести в выпадающий список; как тип — Color сторожит каждый вызов. Диагностика на последней строке: Аргумент типа "жёлтый" нельзя назначить параметру типа "красный" | "зелёный" | "синий".

Работает это только вместе с as const из третьей главы. Уберите его — и весь эффект пропадёт, причём молча.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Без as const тип массива — string[], элемент у него string, и «жёлтый» под это описание подходит. Проверять было нечего: тип честно описывает то, что ему сказали.

as const делает из массива кортеж литералов, [number] складывает их обратно в объединение. Одно без другого не работает.

У кортежа осмысленны и конкретные номера: T[0] — тип первого места, T[0 | 1] — двух первых. И ещё одно поле у кортежа особенное — length.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Тип "4" не может быть назначен для типа "3" — длина кортежа известна компилятору как конкретное число, а не как «какое-то number». У обычного массива length имеет тип number, и такой диагностики не будет.

COLORS — переменная, то есть значение. В типовой позиции она сама по себе не стоит: сначала typeof переводит её в тип, и только потом к этому типу применяются скобки.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Диагностика снова подсказывает решение сама: "COLORS" относится к значению, но здесь используется как тип. Возможно, вы имели в виду "typeof COLORS"?. Скобки вокруг typeof COLORS обязательными не являются — typeof COLORS[number] компилятор разберёт точно так же, — но со скобками порядок чтения виден глазом, и в чужом коде вы чаще встретите именно их.

Индексный доступ возвращает тип — значит, к результату можно применить его снова. Так из одного описания достаётся любая его часть, на любой глубине.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Читается цепочка слева направо, как обычное обращение к данным: у Order берём items, у массива — элемент, у элемента при желании — price. Вложенный тип позиции нигде не назван отдельно, и это нормально: имя ему даёт Item, а источник остаётся один.

Диагностика на последней строке показывает ещё кое-что важное: в тексте ошибки указан не Item, а расписанная форма { title: string; price: number; qty: number; }. Псевдоним прозрачен — компилятор сверяет структуру, а не имя.

Ломается цепочка на необязательном поле, и по хорошей причине.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Текст называет виновника прямо: Свойство "port" не существует в типе "{ port: number; } | undefined". Вопросительный знак в server? добавил в тип undefined, а у undefined поля port действительно нет — тот же strictNullChecks, что и в главе про сужение, только на уровне типов.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

NonNullable — встроенная утилита, одна из тех, что разбираются в следующей статье. Здесь она нужна ровно затем, чтобы убрать undefined перед следующей скобкой.

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

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ограничение K extends keyof Settings из главы про дженерики не даёт передать посторонний ключ, а Settings[K] связывает второй аргумент с первым. В вызове setOption("wrap", ...) параметр K сузился до литерала "wrap", и Settings[K] в тот же миг стал boolean: Аргумент типа "string" нельзя назначить параметру типа "boolean". Строка при этом всё равно записалась — диагностика не отменяет исполнения.

Тот же приём работает и на возврате. Разница между «ключом-параметром» и «ключом из объединения» видна лучше всего рядом.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Обе функции возвращают одно и то же число, но обещают разное. У getLoose в подписи стоит Settings[keyof Settings] — «что-нибудь из значений», поэтому Свойство "toFixed" не существует в типе number | boolean | "dark" | "light", а вложенной причиной идёт Свойство "toFixed" не существует в типе false. У get ключ прошёл параметром и вернулся числом — вызов проходит без слова.

Правило простое: если ключ известен вызывающему, пропускайте его через параметр K, а не через keyof T. Иначе тип результата разъедется с реальностью в первой же строке.

Индексный доступ описывает, что должно лежать по ключу, и никак не проверяет, что там что-то есть. По умолчанию компилятор считает любой элемент массива существующим.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ни одной диагностики — и TypeError: Cannot read properties of undefined. Тип элемента здесь ни при чём: он описывает содержимое, а не длину. Проверку на выход за границы включает отдельный флаг noUncheckedIndexedAccess, до которого доберёмся в главе про настройку; по умолчанию он выключен.

ЗаписьЧто достаётПример
T["поле"]тип одного поляUser["name"] → string
T["a" | "b"]объединение типов нескольких полейUser["id" | "name"] → number | string
T[keyof T]объединение типов всех полейRow[keyof Row] → number | string | boolean
T[number]тип элемента массива(typeof COLORS)[number] → объединение литералов
T[0]тип места в кортежеPoint[2] → string
T["length"]длину кортежа литеральным числомPoint["length"] → 3
T[K]поле по параметру дженерикаSettings[K] → тип именно этой настройки

  • Индексный доступ T["поле"] достаёт тип поля из типа объекта: одно описание вместо копипасты, и связь, которая не разъедется.
  • Работают только квадратные скобки — точка даёт TS2713, а переменная в скобках TS2749: там нужен тип, и typeof его даёт.
  • В скобках может стоять объединение ключей; T[keyof T] — это объединение типов всех значений.
  • T[number] достаёт тип элемента массива, и в паре с as const превращает список значений в объединение литералов.
  • Цепочка индексов идёт на любую глубину, но спотыкается о необязательное поле: в типе появляется undefined, и его убирают NonNullable.
  • В дженерике T[K] связывает ключ и значение — так один get возвращает точный тип, а не объединение всех полей.

1/4Написано type User = { name: string }, следом type Name = User.name. Что скажет компилятор?

2/4Есть const field = "name" и type N = User[field]. Почему это не работает?

3/4Массив объявлен как const TABS = ["профиль", "заказы"], без as const. Каким получится (typeof TABS)[number]?

4/4Тип Shape — это Circle | Square, поле kind есть у обоих, поле r только у круга. Что получится из Shape["kind"] и Shape["r"]?

Подпись без копипасты

Тип покупателя описан в Order, а в параметре функции переписан заново — и уже разъехался с оригиналом: в заказе три поля, в параметре два. Перепишите обе функции так, чтобы форма бралась из Order, а не повторялась, и добавьте в подпись почту.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Оба параметра описываются индексным доступом: покупатель — Order["customer"], позиция — Order["items"][number]. После этого почта оказывается в типе сама, и её можно использовать.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Проверить починку легко: добавьте в items поле qty: number — и titleOf примет новую форму без единой правки, потому что она у него не своя, а взятая из Order. Со старой записью пришлось бы править параметр вручную, а забытая правка обнаружилась бы только на исполнении.

Вкладки из массива

Список вкладок уже есть в коде, но функция принимает любую строку — и опечатка проходит молча. Сделайте так, чтобы имя вкладки бралось из самого списка: второй вызов должен дать TS2345, а список остаться единственным местом, где вкладки перечислены.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

as const уже стоит, значит тип у TABS — кортеж из трёх литералов. Осталось достать из него элемент.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Диагностика: Аргумент типа "настройик" нельзя назначить параметру типа "профиль" | "заказы" | "настройки". Добавьте четвёртую вкладку в массив — тип расширится сам; напишите type Tab = "профиль" | "заказы" | "настройки" руками — и получите второй список, который однажды разойдётся с первым.

Если убрать as const, всё развалится обратно: Tab станет string, и опечатка снова пройдёт. Проверьте это сами — так связка запоминается лучше.

Порт из необязательной секции

Цепочка индексов упирается в вопросительный знак. Прочитайте диагностику и почините объявление типа Port, не трогая AppConfig: секция базы должна остаться необязательной.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Текст диагностики называет тип, к которому применили вторую скобку: Свойство "port" не существует в типе "{ host: string; port: number; } | undefined". Значит, перед следующим шагом из объединения нужно убрать undefined — это делает NonNullable.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Промежуточное имя Db здесь только ради читаемости — можно и одной строкой: type Port = NonNullable<AppConfig["db"]>["port"].

Обратите внимание на последнюю строку: тип Port починился, но сама секция от этого не появилась. Проверка config.db ? по-прежнему обязательна — NonNullable сузил тип, а не заполнил конфигурацию.