Перейти к содержимому
Теория · Операторы типов и утилиты · урок 1 из 8

keyof и доступ по ключу

Чему вы научитесь

Чему вы научитесь:

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

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

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

TypeScript
interface User {
    name: string;
    age: number;
}

function getField(user: User, field: string) {
    return user[field]; // ✗
}
TEXT
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": объединение двух литеральных типов, по одному на каждое свойство. Такую запись можно поставить везде, где нужен тип, например у параметра:

TypeScript
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

Давайте разберём пример по строкам:

  1. field: keyof User разрешает параметру только два значения: "name" и "age".
  2. user[field] теперь компилируется: какое бы имя ни пришло, такое свойство у User есть.
  3. Оба вызова передают настоящие имена и печатают Ann и 30.

Имя, которого у User нет, tsc не пропустит. Вызов getField(ann, "email") даст ошибку:

TEXT
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".

Точный тип в обобщённой функции

Чтобы функция запомнила имя, его делают параметром типа:

TypeScript
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

Разберём сигнатуру по частям:

  1. T — тип объекта, K — имя свойства.
  2. K extends keyof T — ограничение, как в прошлой теме: в K может попасть только одно из имён свойств T.
  3. T[K] — тип результата: тип того свойства, имя которого передали.
  4. В вызове getProp(ann, "name") TypeScript выводит T как User, а K как "name". Результат — User["name"], то есть string, и toUpperCase работает.
  5. Для "age" та же функция возвращает number, и работает toFixed.

Пройдите пример по шагам. В первой функции подсказка раскрывает keyof User через сужение, а во второй показывает, какой тип получает результат getProp:

Вживуюkeyof и тип свойствашаг 1 из 6

Подсказка показывает тип так, как его записали: keyof User.

Нажимайте «Следующий шаг»: подсказка покажет тип на выделенной строке

Функцию вроде getProp часто просят написать на собеседовании: она читает свойство по имени и сохраняет его точный тип. Решение — второй параметр типа K extends keyof T и результат T[K].

Частая ошибка

Второй параметр типа забыли и записали имя свойства прямо как keyof T:

TypeScript
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(); // ✗
TEXT
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 от них ничего не остаётся.