keyof и доступ по ключу
Чему вы научитесь:
- получать имена свойств типа через
keyofи тип свойства черезUser["age"]; - писать функцию, которая принимает только настоящие имена свойств и возвращает точный тип;
- брать тип у готового значения через
typeofи превращать список значений в объединение; - строить новые типы из старых утилитами
Partial,Pick,Omit,Recordи другими.

Представьте генератор анкет. Вы один раз описываете анкету: какие в ней поля и что пишут в каждом. Дальше генератор сам делает из неё новые бумаги: список названий полей, черновик, где любое поле можно оставить пустым, короткую карточку из двух полей. Руками ничего переписывать не нужно, а новое поле анкеты само появится во всех бумагах.
Операторы типов в TypeScript работают так же: они строят новые типы из уже описанных. Начнём с первой бумаги — списка названий полей. Вот задача, где он нужен: функция получает пользователя и имя поля, которое надо показать.
interface User {
name: string;
age: number;
}
function getField(user: User, field: string) {
return user[field]; // ✗
}error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'User'.tsc отказывается читать user[field]. В параметр field с типом string может прийти любая строка: "email", "nmae". Таких свойств у User нет, и компилятор не знает, что вернёт чтение. А молча считать результат any строгий режим не разрешает.
Нужен тип, в который входят только имена свойств User.
Оператор keyof
Оператор keyof берёт объектный тип и возвращает объединение имён его свойств. Для нашего интерфейса keyof User — это "age" | "name": объединение двух литеральных типов, по одному на каждое свойство. Такую запись можно поставить везде, где нужен тип, например у параметра:
interface User {
name: string;
age: number;
}
function getField(user: User, field: keyof User) {
return user[field];
}
const ann: User = { name: "Ann", age: 30 };
console.log(getField(ann, "name")); // Ann
console.log(getField(ann, "age")); // 30Давайте разберём пример по строкам:
field: keyof Userразрешает параметру только два значения:"name"и"age".user[field]теперь компилируется: какое бы имя ни пришло, такое свойство уUserесть.- Оба вызова передают настоящие имена и печатают
Annи30.
Имя, которого у User нет, tsc не пропустит. Вызов getField(ann, "email") даст ошибку:
error TS2345: Argument of type '"email"' is not assignable to parameter of type 'keyof User'.Две особенности того, как TypeScript показывает такие типы:
- в подсказке редактора и в сообщениях об ошибках тип остаётся таким, как его записали:
keyof User, а не список имён; - когда TypeScript 7 печатает объединение литералов, он сортирует варианты, поэтому
"age"стоит раньше"name", хотя в интерфейсе первым объявленоname.
Порядок вариантов в объединении ничего не значит: "age" | "name" и "name" | "age" — один и тот же тип.
Здесь генератор анкет перестаёт быть похожим на keyof. Бумажный список полей можно взять в руки, а keyof User существует только для tsc и стирается из JavaScript. Узнать имена свойств при запуске можно только у настоящего объекта, например через Object.keys.
Тип свойства по имени
Квадратные скобки работают и в типах. Запись User["age"] — это доступ по ключу: тип свойства age у типа User. В коде ann["age"] достаёт значение свойства, а в типе User["age"] достаёт его тип:
User["age"]— этоnumber;User["name"]— этоstring;User[keyof User]— этоstring | number: в скобках стоит объединение имён, и получается объединение типов всех свойств;User["email"]не скомпилируется:error TS2339: Property 'email' does not exist on type 'User'.
Теперь понятно, что возвращает getField из примера выше. В user[field] может прийти любое имя из keyof User, поэтому у результата тип User[keyof User], то есть string | number. Вызов getField(ann, "name").toUpperCase() не скомпилируется: функция не помнит, что ей передали именно "name".
Точный тип в обобщённой функции
Чтобы функция запомнила имя, его делают параметром типа:
interface User {
name: string;
age: number;
}
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const ann: User = { name: "Ann", age: 30 };
console.log(getProp(ann, "name").toUpperCase()); // ANN
console.log(getProp(ann, "age").toFixed(1)); // 30.0Разберём сигнатуру по частям:
T— тип объекта,K— имя свойства.K extends keyof T— ограничение, как в прошлой теме: вKможет попасть только одно из имён свойствT.T[K]— тип результата: тип того свойства, имя которого передали.- В вызове
getProp(ann, "name")TypeScript выводитTкакUser, аKкак"name". Результат —User["name"], то естьstring, иtoUpperCaseработает. - Для
"age"та же функция возвращаетnumber, и работаетtoFixed.
Пройдите пример по шагам. В первой функции подсказка раскрывает keyof User через сужение, а во второй показывает, какой тип получает результат getProp:
1interface User { name: string; age: number }2 3function label(key: keyof User) {подсказка редактораkey: keyof User4 if (key === "name") {5 return "Full " + key;6 }7 return "Your " + key;8}9 10function getProp<T, K extends keyof T>(11 obj: T,12 key: K,13): T[K] {14 return obj[key];15}16const u: User = { name: "Ann", age: 30 };17const n = getProp(u, "name");18const a = getProp(u, "age");
Подсказка показывает тип так, как его записали: keyof User.
Функцию вроде getProp часто просят написать на собеседовании: она читает свойство по имени и сохраняет его точный тип. Решение — второй параметр типа K extends keyof T и результат T[K].
Частая ошибка
Второй параметр типа забыли и записали имя свойства прямо как keyof T:
interface User {
name: string;
age: number;
}
function getProp<T>(obj: T, key: keyof T): T[keyof T] {
return obj[key];
}
const ann: User = { name: "Ann", age: 30 };
getProp(ann, "name").toUpperCase(); // ✗error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.Функция снова не помнит, какое имя ей передали. T[keyof T] — это объединение типов всех свойств, string | number, а у числа метода toUpperCase нет. Исправление показано выше: имя становится отдельным параметром типа K, а результат — T[K].
Главное
keyof User— объединение имён свойствUser:"age" | "name".User["age"]— тип свойства по имени,number;User[keyof User]— объединение типов всех свойств.<T, K extends keyof T>(obj: T, key: K): T[K]принимает только настоящие имена и возвращает тип именно этого свойства.keyofиT[K]проверяет только tsc: в JavaScript от них ничего не остаётся.