Ключи и индексированный доступ
Сейчас функция может вернуть весь курс по идентификатору. Иногда нужна общая операция чтения одного поля: заголовка, количества уроков или необязательного описания. Если разрешить произвольную строку в качестве имени, вызывающая сторона легко получит опечатку. Если вернуть объединение всех типов полей, потеряется точность конкретного выбора.
Свяжем ключ с типом его значения. Оператор keyof описывает допустимые ключи типа, а индексированный доступ T[K] — тип поля, выбранного этим ключом. На том же курсе увидим, почему title даёт строку, а lessons число, хотя используется одна функция. JavaScript-чтение свойства остаётся обычной операцией.
Ключи существующей модели
У Course есть id, title, topic, lessons, url и необязательное description. Тип keyof Course объединяет имена этих полей. Он не вызывает Object.keys и не создаёт массив строк во время выполнения. Это информация для проверки исходника, поэтому обход реального объекта остаётся отдельным действием.
Тип Course['title'] обозначает строковый тип названия. Course['lessons'] обозначает число. При чтении необязательного description необходимо учитывать отсутствие. Доступ по ключу в описании типа не читает настоящий объект: мы устанавливаем связь, которой позднее воспользуется подпись функции.
В lesson-11 сохраните модель и данные предыдущего шага. Полный src/app.ts:
import { courses } from './data.js';
function field<T, K extends keyof T>(value: T, key: K): T[K] {
return value[key];
}
const course = courses[0];
if (course !== undefined) {
const title = field(course, 'title');
const lessons = field(course, 'lessons');
console.log(title.toUpperCase());
console.log(lessons.toFixed(0));
}
Ожидается заглавное название JavaScript и строка «20». У первого результата доступен toUpperCase, у второго toFixed. Эти методы не выбраны случайно: литеральный ключ в конкретном вызове определяет тип результата функции.
K extends keyof T означает, что ключ относится к объекту данного T. Возвращаемое T[K] сохраняет связь между ними. Тело функции просто возвращает value[key], не добавляя обходов или преобразований. Правильность отношения получается из подписи и того, что реализация возвращает именно выбранное свойство.
Точность конкретного ключа
Если вместо field(course, 'title') вызвать field(course, 'lessons'), результат становится числовым. Но если переменная ключа сама может быть либо title, либо lessons, результат также допускает строку или число. Инструмент не угадывает выбор ветки, которую автор ещё не установил.
Рассмотрим самостоятельный фрагмент внутри существующей проверки первого курса:
const key: 'title' | 'lessons' = course.lessons > 16 ? 'title' : 'lessons';
const value = field(course, key);
if (typeof value === 'string') console.log(value.toUpperCase());
else console.log(value.toFixed(0));
Для JavaScript условие выбирает название, поэтому ожидается заглавная строка. Но тип значения до typeof учитывает оба допустимых ключа. Проверка отделяет способы обработки. Это продолжает механизм сужения из пятого урока, только состав объединения теперь получен через индексированный доступ.
При неизвестном ключе, пришедшем извне, понадобится проверка строки. Запись key as keyof Course не подтверждает существование поля. Особенно опасно автоматически объявлять любой ключ из URL допустимым: человек может передать titel или другой неподдерживаемый вариант. Для конечного интерфейса часто лучше иметь небольшой явный список разрешённых полей.
Отсутствующий объект и отсутствующее поле
Мы сначала проверяем courses[0] !== undefined. Даже идеальная связь ключа и типа не создаёт первый курс. Настройка noUncheckedIndexedAccess напоминает об этом на уровне массива. Только внутри ветки, где объект есть, вызов field имеет корректный аргумент.
Необязательное описание — другой случай отсутствия. Объект существует, но поле может не иметь значения. Вызов field(course, 'description') сохраняет эту возможность в результате. Следовательно, вызов строкового метода потребует запасной строки или проверки. Общая функция не должна превращать необязательное свойство в обязательное ради удобного интерфейса.
Эти две границы легко смешать. Проверка наличия курса позволяет прочитать его обязательное название, но не доказывает наличие описания. Проверка описания, наоборот, не нужна для чтения обязательного количества. Хорошая подпись помогает оставить каждую проверку там, где она относится к действительному правилу.
Почему Object.keys не обещает keyof
Тип объекта часто является неполным представлением более богатого значения. В третьем уроке мы передавали переменную с дополнительным полем. При выполнении Object.keys может перечислить такие фактические поля, которых нет в статическом Course. Поэтому нельзя автоматически считать каждую возвращённую строку точным keyof Course только из-за типа переменной.
Для таблицы интерфейса безопаснее заранее определить, какие поля показываются. Это не обязательно все собственные ключи объекта. Например, id и URL могут храниться в данных, а отображать нужно только название, тему и количество. Такой список является продуктовым решением, а не побочным эффектом структуры объекта.
Если нужно отобразить произвольные данные, контракт будет другим: словарь неизвестных значений, проверка каждого значения и правила отображения. Универсальный динамический инспектор и типизированная таблица курса решают разные задачи. Не стоит делать вторую менее точной только для имитации первой.
Разрешённый ключ не равен разрешённой операции
Допустим, настройки таблицы разрешают показывать только title, topic и lessons. Все эти имена относятся к keyof Course, но не каждый ключ курса является видимой колонкой. Более узкий тип VisibleField должен выразить продуктовый набор. Общий field сможет читать любое корректное поле, а интерфейс ограничит свой параметр отдельно.
Такое разделение полезно при обработке URL. Посетителю не нужно автоматически открывать все будущие поля модели лишь потому, что они появились в Course. Модель данных может расширяться, а публичный набор действий должен изменяться сознательно. Связь ключа и значения обеспечивает точность чтения, но не является политикой доступа.
Ещё один случай — сериализованный ключ lessons. После его проверки функция вернёт число, однако отображаемая подпись всё равно строковая. Преобразование числа в текст должно произойти в представлении. Тип выбранного поля не включает автоматически суффикс «уроков» и не учитывает правила склонения.
Если параметр ключа приходит как объединение двух имён, можно сузить сам ключ до конкретной ветки и выполнить отдельный вызов внутри неё. Это нередко яснее, чем работать со слишком широким результатом позднее. Но не следует утверждать, что проверка уже сохранённого ключа всегда автоматически связывает отдельно сохранённое значение с его прошлым происхождением. Для надёжной читаемости используйте вызов в ветке или прямо сужайте значение по typeof, как в нашем примере.
Последняя деталь касается методов объекта. keyof может включать их имена, если они заявлены в форме, а индексированный доступ тогда обозначит тип функции. Чтение такого поля не является его вызовом. У нашей простой записи методов нет; мы ограничились данными курса, чтобы не смешивать контракт значения и вопрос привязки this при последующем выполнении функции.
Оператор keyof и индексированный доступ описаны в первичной документации. Наша функция связывает объект, допустимый ключ и соответствующий тип результата. Исходник не запускался при подготовке; ожидаемые строки получены из первой записи каталога.
Попробуйте заменить ключ на description и рядом дать запасную подпись через ??. Вы должны увидеть, как точность связи сохраняет именно ту неопределённость, которая есть в модели. В следующем уроке будем применять весь набор ключей, чтобы автоматически построить форму настроек видимости полей, сохраняя её согласованной с Course.