ПрограммированиеFullstack разработчик

Как работает оператор keyof в TypeScript, для чего он используется и как влияет на проектирование безопасных функций и API?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

История вопроса

TypeScript с самого начала разрабатывался для строгой типизации объектов и написания более предсказуемого кода на JavaScript. Одним из инструментов для обеспечения безопасности при доступе к свойствам объектов стал оператор keyof, который появился в TypeScript 2.1. Он позволяет создавать типы, представляющие набор строковых (и символьных) ключей доступных в заявленном типе объекта.

Проблема

В чистом JavaScript свойства объекта могут запрашиваться по любой строке, и если ошибиться — получится undefined или ошибка, которая будет найдена только во время выполнения. Без строгой типизации легко сделать опечатку или забыть об обновлении строки-ключа при рефакторинге. Кроме того, часто требуется построить функцию, принимающую только корректные ключи конкретного объекта или типа.

Решение

Оператор keyof создает union-тип из всех ключей типа. С его помощью можно ограничивать допустимые ключи, поднимать безопасность API (например, для getter/setter функций), и создавать универсальные утилитарные типы.

Пример кода:

type User = { name: string; age: number; active: boolean }; type UserKey = keyof User; // "name" | "age" | "active" function getProp<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user: User = { name: 'Tom', age: 33, active: true }; const age = getProp(user, 'age'); // возраст типа number

Ключевые особенности:

  • Позволяет объявлять "ключ"-типы, зависящие от структуры объекта.
  • Делает функции типа get/set свойств максимально типобезопасными — исключены опечатки и несуществующие ключи.
  • Работает и с символьными свойствами (symbol), начиная с TypeScript 2.7 и выше.

Вопросы с подвохом.

Что возвращает keyof у массива и можно ли получить индексы?

keyof для массива возвращает типы ключей объекта, которыми массив на самом деле и является в JS. Обычно это строки индексов и специальные свойства массива.

Пример кода:

type A = keyof number[]; // "length" | "toString" | "pop" | ... | number

Может ли keyof вернуть ключи у union-types?

Да, keyof от union-типа возвращает пересечение ключей обоих объектов, а не их объединение.

Пример кода:

type A = {a: string, b: number } type B = {b: number, c: boolean } type C = keyof (A | B); // "b"

Что произойдет если свойство объекта опциональное? Поддерживает ли keyof "?"?

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

Пример кода:

type D = { a: string; b?: number }; type DK = keyof D; // "a" | "b"

Типовые ошибки и анти-паттерны

  • Использовать константные строки для ключей вместо keyof — потеря типизации.
  • Считать что keyof для union возвращает объединение, а не пересечение.
  • Ожидание, что keyof учитывает только явно объявленные ключи, но не унаследованные.

Пример из жизни

Негативный кейс

Пишут функцию getProperty(obj, key) с ключом строкового типа, вызывают с несуществующим ключом — ошибка появляется только во время выполнения.

Плюсы:

  • Универсальный код.

Минусы:

  • Нет проверки на этапе компиляции, возможны ошибки при рефакторинге, уязвимость для опечаток.

Позитивный кейс

Используют дженерики: getProperty<T, K extends keyof T>(), тип key строго ограничен ключами структуры.

Плюсы:

  • Абсолютная типобезопасность, ошибка опечатки или ошибки в ключе невозможна.

Минусы:

  • Код немного сложнее, дженерики не всегда понятны новичкам.