keyof и typeof: тип из значения и его ключи
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
У слова typeof в TypeScript две работы. В выражении это тот же оператор, что и в части I: отвечает "object", "number" и не более того. В позиции типа — совсем другая операция: она снимает с готового значения его тип целиком. Рядом живёт keyof, который превращает тип объекта в объединение его ключей. Вместе они дают приём, после которого списки в коде перестают писать дважды.
Компилятор читает файл в двух режимах. После двоеточия, после type, внутри угловых скобок — это мир типов. Всё остальное — мир значений. Одно и то же слово в этих мирах может означать разное, и typeof — самый заметный такой случай.
В console.log сработал знакомый оператор — тот, что честно отвечает object и не умеет отличить объект от массива. В строке с type тот же typeof сделал другое: заглянул в settings и выдал его тип целиком — { theme: string; fontSize: number }. Дальше Settings живёт как обычный именованный тип, и объявление broken получило за это TS2322: Тип "string" не может быть назначен для типа "number".
Обратите внимание на вторую половину вывода: строка "14" всё равно легла в объект и напечаталась. Компилятор высказался и отошёл в сторону — до выполнения от типов не доезжает ничего, ровно как с readonly-полем из главы про объекты.
Снять тип можно не только с объекта. Функция — тоже значение, и у неё есть тип: список параметров и то, что она возвращает.
Параметры v и s не размечены, и всё же неявного any здесь нет: типы пришли из Formatter сверху вниз — контекстная типизация из статьи про вывод типов. А вызов с "нет" получил TS2345, но выполнился: непустая строка истинна, поэтому в консоли восклицательный знак.
Что именно вывел typeof, проще всего увидеть наведением курсора в редакторе — подсказка покажет развёрнутый тип, а не само выражение.
typeof отдаёт ровно тот тип, который компилятор вывел для значения. А выводится он по-разному в зависимости от того, как объявлено имя.
let можно переприсвоить, поэтому его тип расширяется до string. У const расширять нечего — остаётся литеральный тип "dark". Это та самая разница из статьи про литеральные типы, и typeof просто наследует её.
keyof работает только в мире типов и делает одну вещь: берёт тип объекта и отдаёт объединение его ключей. Не string, а перечисление конкретных имён — каждое из них литеральный тип.
keyof User — это "id" | "name" | "email", обычное объединение из главы про сужение типов. Диагностика TS2322 напечатала его не раскрывая: Тип "age" не может быть назначен для типа "keyof User". Компилятор часто оставляет короткую запись, если она читается лучше развёрнутой; развёрнутый вариант всегда покажет редактор при наведении.
Главное про keyof — это не «список строк». Это список, привязанный к типу: поменяли User, и UserKey изменился в ту же секунду, а все места, где ключи перечислялись, компилятор проверит заново.
Чаще всего ключи нужны не от интерфейса, а от конкретного объекта — словаря, таблицы настроек, набора обработчиков. Первое, что хочется написать, — keyof settings. Так нельзя, и причина ровно та, с которой начиналась статья.
Две зеркальные диагностики. TS2749: "settings" относится к значению, но здесь используется как тип — и тут же подсказывает недостающее слово — Возможно, вы имели в виду "typeof settings"? TS2693: "Config" относится только к типу, но используется здесь как значение — интерфейс существует лишь на этапе проверки, брать у него typeof нечего.
Отсюда и связка keyof typeof. Читается справа налево: typeof settings достаёт тип из значения, keyof берёт у этого типа ключи. Два оператора, две ступени, и ни одной строки в собранном коде.
TS2345 развернул объединение полностью: Аргумент типа "GBP" нельзя назначить параметру типа "RUB" | "USD" | "EUR". Список валют написан один раз — в самом объекте; тип подтянулся сам. Добавьте туда четвёртую строку, и Currency расширится без единой правки.
Вторая строка вывода — про то, чего типы не могут. Предупреждение проигнорировали, выполнение продолжилось, обращение к несуществующему ключу вернуло undefined. Функция обещала string и вернула undefined — компилятор не соврал, просто он проверяет исходник, а не то, что происходит с ним потом.
Ключи описаны типом, функция их принимает — казалось бы, перебор словаря теперь безопасен насквозь. Спрогнозируйте, что скажет компилятор.
Аргумент типа "string" нельзя назначить параметру типа "RUB" | "USD" | "EUR". Обидно: объект прямо здесь, ключи известны, а key оказался просто строкой. Дело в сигнатуре: Object.keys объявлена как возвращающая string[] — без всякой связи с тем, что ей передали.
Это не недосмотр, а следствие структурной типизации из главы про объекты. Тип обещает, какие поля у объекта есть, но не обещает, что других нет. Значит, во время выполнения ключей может оказаться больше, чем в типе, — и честно назвать их keyof невозможно.
Ни одной диагностики — и BTC undefined в консоли. Утверждение as Code[] сказало компилятору «здесь только мои ключи», проверять он это не стал, а объект пришёл с третьим полем. Проверка лишних свойств срабатывает на литералах, а fromServer положили через переменную — и она промолчала.
Компилятор молчит не потому, что всё хорошо, а потому, что вы попросили его молчать. Каждый as — обещание, которое дальше держите вы.
Если ключи заранее известны, надёжнее объявить их списком с явным типом: тогда расхождение со словарём ловится сразу, а не превращается в undefined посреди цикла.
Список приходится держать рядом со словарём, зато он под присмотром: удалили ключ из rates — красная строка появится в CODES. Для объекта, который вы сами написали в этом же файле, as Code[] тоже приемлемо; опасность начинается там, где объект пришёл извне.
У keyof простое правило и несколько неочевидных следствий. Первое встречается сразу, как только под руку попадает тип с индексной сигнатурой.
Компилятор промолчал на обеих строках: keyof от индексной сигнатуры со строковым ключом — это string | number. Число попало в набор не по ошибке. В JavaScript ключом бывает строка или символ, а число приводится к строке: obj[0] — то же самое, что obj["0"]; раз запись с числом разрешена, число обязано входить в объединение ключей.
Второе следствие — про объединения. Тут интуиция подсказывает «сложить ключи», а компилятор делает наоборот.
Тип "radius" не может быть назначен для типа "kind" — от объединения keyof оставил только общие ключи. И это единственный безопасный ответ: значение типа Shape может оказаться квадратом, у которого никакого radius нет. Если общих полей у вариантов не окажется вовсе, keyof честно вернёт never — тип без единого значения.
keyof видит только те ключи, которые известны типу. Поля, приделанные объекту во время выполнения, в объединение не попадут — про них компилятор ничего не знает.
Массив — тоже объект, и ключи у него не только числовые: методы map, filter, свойство length — всё это его поля.
Обе строки прошли. Поэтому keyof к массивам почти не применяют: для индекса берут number, а чтобы получить объединение самих элементов, пишут (typeof list)[number] — запись из следующей статьи.
| Тип | keyof от него |
|---|---|
{ id: number; name: string } | "id" | "name" |
{ [key: string]: number } | string | number |
{ [key: number]: string } | number |
A | B | только общие ключи, иначе never |
string[] | number и все имена методов массива |
any | string | number | symbol |
Всё это существует ради одной привычки: не описывать одни и те же данные дважды. Рукописный список всегда рано или поздно расходится с объектом — просто потому, что правят их в разное время.
Оба обращения к icons сработали одинаково — но одно из них компилятор считает ошибкой, потому что сверяется с устаревшим списком. Тип Derived такой проблемы иметь не может в принципе: он не копия объекта, а его отражение.
Четыре записи закрывают почти все повседневные случаи. Последняя достаёт из типа не ключи, а сами значения — её подробно разбирает следующая статья.
| Что нужно | Как записать |
|---|---|
| тип готового значения | typeof value |
| ключи типа | keyof Type |
| ключи готового объекта | keyof typeof value |
| значения готового объекта | (typeof value)[keyof typeof value] |
И ещё одно место, где keyof встречается постоянно, — ограничения дженериков: запись K extends keyof T означает «ключ, который у этого объекта действительно есть». Идея та же самая, просто тип, у которого берут ключи, приходит параметром.
- В позиции типа
typeof— не оператор из части I: он снимает с готового значения его выведенный тип целиком, включая функции. keyofберёт тип объекта и отдаёт объединение его ключей — литеральные типы имён, а неstring.keyofждёт тип, а переменная — значение; отсюдаTS2749и связкаkeyof typeof value, которая читается справа налево.Object.keysвсегда возвращаетstring[]: тип не запрещает объекту иметь лишние поля, поэтому честногоkeyofтам быть не может.- У индексной сигнатуры
keyofдаётstring | number, у объединения — только общие ключи, а при их отсутствииnever. - Смысл всей пары — один источник правды: список пишется в объекте, а тип выводится из него и не может отстать.
1/4В коде есть const box = { w: 10, h: 20 };. Автор пишет type K = keyof box;. Что произойдёт?
2/4Объявлены const rates = { RUB: 1, USD: 95 }; и type Code = keyof typeof rates;. Какого типа будут элементы у Object.keys(rates)?
3/4Есть type A = { id: number; note: string } и type B = { id: number; total: number }. Что даст keyof (A | B)?
4/4let mode = "dark"; и const fixed = "dark";. Чем отличаются typeof mode и typeof fixed?
Роли в одном месте
Словарь ролей пополнили, а список допустимых значений — нет, и вызов с новой ролью не проходит. Исправьте объявление Role так, чтобы список больше никогда не приходилось дописывать вручную. Трогать сам словарь и функцию не нужно.
Список ключей у нас уже есть — это сам объект ROLES. Осталось попросить его у компилятора: typeof ROLES даёт тип объекта, keyof — объединение его ключей.
Теперь Role — это "admin" | "editor" | "viewer", обе строки выполняются без диагностик. Добавили в ROLES четвёртую роль — тип расширился сам; удалили роль — компилятор покажет все места, где на неё ссылались.
Перебрать словарь
Цикл должен напечатать каждую валюту с её курсом, но компилятор ругается на show(key). Объясните, почему тип Code здесь не помогает, и почините перебор двумя способами: коротким и безопасным.
Object.keys объявлена как возвращающая string[] — она не смотрит на переданный объект, и тип ключей теряется. Короткий способ — утверждение типа:
Компилятор замолчит, но проверять ничего не будет: если в объект попадёт лишнее поле, show получит ключ, которого в типе нет. Для объекта, написанного тут же рядом, это приемлемо.
Безопасный способ — перебирать проверенный список, а не сам объект:
Здесь каждая строка списка сверена с ключами rates: опечатка вроде "UDS" станет ошибкой прямо в объявлении.
Ключи двух питомцев
Тип PetKey должен принимать любое поле, встречающееся хоть у кошки, хоть у собаки. Сейчас вторая строка не проходит. Почините объявление и объясните, почему keyof от объединения ведёт себя именно так.
keyof от объединения оставляет только общие ключи — здесь это одно поле name, поэтому строка с "bark" получает TS2322. Иначе и быть не может: значение типа Pet вполне может оказаться кошкой, у которой поля bark нет, и обещать доступ к нему компилятор не вправе.
Нужный набор получается объединением результатов, а не keyof от объединения:
Теперь PetKey — это "name" | "meow" | "bark", обе строки проходят. Важно помнить, что такой ключ уже не гарантирует доступ к полю у конкретного питомца — он лишь перечисляет имена.