keyof принимает объектный тип и даёт объединение имён его свойств. keyof User ниже это тип "id" | "name" | "email", поэтому переменная этого типа может хранить только одну из этих трёх строк.
keyof существует только в системе типов. Он стирается при компиляции кода, поэтому ничего не проверяет во время выполнения. Зато он даёт гарантию времени компиляции, что ключ называет настоящее свойство: переименуйте email в интерфейсе, и каждая строка "email" с типом keyof User станет ошибкой.
keyof typeof: ключи обычного объекта
keyof работает с типами, а не со значениями. Для объекта, записанного как значение, сначала получите его тип через typeof, а затем возьмите ключи:
Читайте изнутри наружу: typeof colors это { red: string; green: string; blue: string }, а keyof от него это объединение ключей. Запись keyof colors без typeof завершается ошибкой error TS2749: 'colors' refers to a value, but is being used as a type here. Did you mean 'typeof colors'?.
Это обычный способ получить объединение из таблицы соответствий: добавьте цвет в объект, и тип Color вырастет вместе с ним. У typeof есть и второе значение, времени выполнения; страница о typeof описывает оба.
Типизированный доступ к свойствам через дженерики
Чаще всего keyof используют в ограничении дженерика. K extends keyof T говорит «K это один из ключей T», а T[K] это тип свойства по этому ключу:
Второй параметр типа важен. С key: keyof T и без K типом возврата будет T[keyof T], объединение всех типов свойств (здесь string | number | boolean), поэтому title.toUpperCase() завершился бы ошибкой error TS2339: Property 'toUpperCase' does not exist on type 'string | number | boolean'. Сохранение точного ключа в K сохраняет точный тип свойства. T[K] это тип доступа по индексу, описанный на странице о типах доступа по индексу.
Индексация обычным string
Очень распространённая ошибка появляется, когда объект индексируют строкой string, пришедшей откуда-то ещё:
Компилятор выводит:
index.ts(10,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Settings'.
No index signature with a parameter of type 'string' was found on type 'Settings'.
В read может попасть любая строка, включая "colour", а Settings ничего не говорит о таком ключе. Есть два исправления: принимать только настоящие ключи или проверять неизвестную строку перед использованием.
name is keyof Settings это предикат типа: когда функция возвращает true, TypeScript считает input ключом внутри if. Предикаты подробно объясняет страница о защитниках типов.
keyof с сигнатурами индекса: string | number
Для типа с сигнатурой индекса keyof возвращает тип индекса, а строковая сигнатура индекса даёт string | number, а не только string:
JavaScript преобразует каждый числовой ключ свойства в строку, поэтому scores[42] и scores["42"] это одно и то же свойство. TypeScript моделирует это, включая number. Вывод показывает ещё одно правило JavaScript: целочисленные ключи перечисляются первыми, по возрастанию, раньше остальных строковых ключей. Когда обобщённой функции нужны только строковые ключи, пишите Extract<keyof T, string> или string & keyof T; у Exclude и Extract есть отдельная страница.
Результаты keyof коротко
| Тип | keyof даёт |
|---|---|
{ id: number; name: string } | "id" | "name" |
{ [key: string]: number } | string | number |
{ [index: number]: string } | number |
{} | never |
any | string | number | symbol |
{ a: 1; b: 2 } | { a: 3; c: 4 } | "a" (ключи, которые есть у каждого члена) |
{ a: 1 } & { c: 4 } | "a" | "c" (ключи любого из них) |
string[] | number плюс имена всех методов массива ("length", "push", ...) |
Строки про объединение и пересечение поначалу кажутся перевёрнутыми. Значение типа объединения может оказаться любым членом, поэтому безопасны только ключи, которые есть в обоих. У значения типа пересечения есть всё от обоих, поэтому у него все ключи.
Почему Object.keys возвращает string[]
Object.keys(obj) типизирован как string[], а не (keyof T)[]. Причина в структурной типизации: у значения может быть больше свойств, чем в его объявленном типе.
p имеет тип Point, но во время выполнения у него есть ключ z. Если бы Object.keys обещал ("x" | "y")[], код, полагающийся на это, был бы неверен для p. Приведение безопасно для объектных литералов, которые вы создали сами; для объектов, приходящих извне, оставляйте ключи как string и проверяйте их.
keyof в сопоставленных типах
keyof это ещё и способ пройти по ключам типа, чтобы построить новый тип. Так написаны встроенные служебные типы Partial и Readonly:
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
interface User {
id: number;
name: string;
}
type UserDraft = MyPartial<User>; // { id?: number; name?: string }
[K in keyof T] посещает каждый ключ, а T[K] читает тип свойства по этому ключу. У сопоставленных типов есть отдельная страница с модификаторами и переназначением ключей.
Часто задаваемые вопросы
Что делает keyof в TypeScript?
keyof T даёт объединение имён свойств типа T. Для interface User { id: number; name: string } keyof User это "id" | "name". Он работает только с типами, при компиляции, и не порождает JavaScript.
Что такое keyof typeof в TypeScript?
keyof нужен тип, а обычный объект вроде const colors = { red: "#f00" } это значение. typeof colors превращает значение в его тип, а keyof typeof colors затем даёт объединение его ключей, здесь "red". Запись keyof colors завершается ошибкой TS2749.
Почему keyof возвращает string | number?
Когда у типа есть строковая сигнатура индекса вроде { [key: string]: number }. JavaScript преобразует числовые ключи свойств в строки, поэтому obj[42] тоже корректное обращение, и TypeScript включает number в тип ключа. Используйте Extract<keyof T, string> или string & keyof T, если нужны только строковые ключи.
Почему Object.keys возвращает string[], а не (keyof T)[]?
Потому что у объекта может быть больше свойств, чем перечисляет его тип: значение с лишним полем можно присвоить более узкому типу. Типизировать результат как (keyof T)[] было бы неправдой для таких объектов. Если вы знаете, что у объекта ровно объявленные ключи, используйте приведение: Object.keys(obj) as (keyof typeof obj)[].
Как исправить "Element implicitly has an 'any' type because expression of type 'string' can't be used to index type"?
Индекс это обычный string, а у объектного типа нет строковой сигнатуры индекса. Типизируйте параметр как keyof YourType вместо string, или сначала проверьте строку защитником типа, который сужает её до keyof YourType, или задайте объекту сигнатуру индекса либо тип Record<string, V>, если он действительно принимает любой ключ.