Рендер и доводка настроек

проект≈ 11 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Модель наконец встречается с экраном. DOM — вторая граница программы: он отвечает так же уклончиво, как чужой JSON, только вместо unknown здесь Element | null, string | undefined и EventTarget. Пройдём каталог от записи до узла и обратно, а потом затянем конфиг и разберём каждую всплывшую диагностику.

Разделы темы:·

Доска задач из части II перерисовывалась целиком из массива: данные решают, разметка отражает. Каталог устроен так же, но карточка теперь собирается из полей Product, и каждое имя на этом пути проверено.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Тип HTMLElement у cardOf написан для читателя — компилятор вывел бы его сам. А вот createElement знает больше, чем кажется: createElement("input") отдаёт HTMLInputElement со всеми его полями, а createElement("article") — просто HTMLElement, потому что своего интерфейса у этого тега нет.

Проверка имён — не абстрактная польза. Опечатка в поле не ломает страницу: карточка соберётся и будет спокойно врать.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

product.stock — это undefined, undefined ложно, и товар в наличии показан как отсутствующий. Компилятор находит это до запуска и даже подсказывает верное имя: «Свойство "stock" не существует в типе "Product". Вы имели в виду "inStock"?».

Соберите ту же карточку строкой через innerHTML — и проверка исчезнет: внутри кавычек компилятор не читает ничего, там снова просто текст.

querySelector объявлен как Element | null — и это первая настоящая ссора с DOM: вы-то знаете, что #catalog в разметке есть.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Две диагностики — и работающая страница. Выполнение они не отменили: типы стираются, и tsc по умолчанию выпускает код даже с ошибками. Но придирки здесь нет. Разметку компилятор не читал и читать не может: поменяется вёрстка, переименуется идентификатор, шаблон соберётся по-другому — и null станет реальностью. С ?. такая опечатка даже не упадёт, а молча ничего не нарисует.

Поэтому узел, без которого рендер бессмыслен, разумно требовать громко — один раз, на старте.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Тип E у mustFind выбирает вызывающий, и никто его не проверяет — та же ловушка дженериков, что в главе про них: подпись удобная, а ответственность на вас.

У querySelector есть перегрузка по имени тега: для строки "input" она отдаёт HTMLInputElement | null. Но разбирать селектор целиком компилятор не берётся.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Отсюда практический вывод: если нужно именно value, ищите по тегу и уточняйте поиск контекстом — form.querySelector("input") вместо document.querySelector("input#search"). Либо проверяйте instanceof HTMLInputElement: в отличие от as, это настоящая проверка, а не обещание.

Обратный путь начинается с делегирования — одного обработчика на контейнере, как на доске задач в части II. Только теперь между кликом и записью каталога стоят три типа, и каждый требует ответа.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

«Свойство "closest" не существует в типе "EventTarget"» звучит странно ровно до того момента, как вспомнишь: target — не элемент. Событие приходит и от window, и от XMLHttpRequest, и от любого объекта, умеющего принимать события; общий контракт EventTarget знает только про подписку и отправку.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ступеней ровно три, и ни одна не лишняя. instanceof HTMLElement сужает EventTarget | null до элемента — тот же приём, что в главе про сужение. closest отдаёт Element | null: карточки над кликом может не быть вовсе. А dataset — это DOMStringMap с индексной сигнатурой, поэтому card.dataset.id имеет тип string | undefined, и find следом честно возвращает Product | undefined.

closest<HTMLElement> — утверждение в маске дженерика: компилятор поверит на слово. Если карточкой может оказаться что угодно, надёжнее ещё один instanceof.

Пока каталог писался с нуля, строгость не мешала: код рождался уже с типами. Но в проект приезжает чужое — функции из части II, наброски из консоли, кусок соседнего модуля. Строка "strict": true включает восемь проверок разом (в главе про настройку они разобраны по одной), и на готовом коде всплывают три класса диагностик.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Первый класс — неявный any. Функция работает и печатает что надо; компилятор всего лишь отказывается угадывать, что ей передадут. Одна аннотация product: Product убирает диагностику и заодно включает проверку полей внутри функции.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Второй класс — то, чего может не быть. find объявлен как Product | undefined, и это не формальность: товара p9 в каталоге нет, и на запуске код падает ровно там, куда показал компилятор. Такой пример полезно досмотреть до конца: сначала диагностика, потом TypeError — одна ошибка, пойманная дважды.

В catch попадает что угодно: throw "строка" никто не запрещал. Поэтому строгость объявляет переменную ошибки как unknown, и обращение к .message становится диагностикой — той же, что в статье про unknown на границе программы.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Текст сообщения у каждого движка свой, а вот лечение общее: спросить, что поймали, вместо того чтобы утверждать.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
ДиагностикаОткуда она в каталогеЧем закрываем
TS7006 «Параметр "product" неявно имеет тип "any"»функции рендера, переехавшие из части IIаннотация параметра или алиас типа для колбэка
TS18047, TS18048 «Возможно, ... имеет значение null / undefined»querySelector, closest, findпроверка на входе: if (node === null) return или ?.
TS18046 «"err" относится к типу unknown»catch вокруг разбора данныхinstanceof Error или предикат из главы про сужение
TS2339, TS2551 «Свойство ... не существует»опечатка в имени поля, EventTarget вместо элементаверное имя или сужение до нужного типа

Порядок доводки скучный и рабочий: включили — починили всё, что всплыло, — закоммитили. Если код большой, а времени мало, strict разбирается на отдельные флаги: noImplicitAny первым, strictNullChecks следом — он даёт самый длинный список и самую большую пользу.

Строгость — не потолок. В каталоге после неё остаются три дыры, и все три видны на запуске.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Record<string, Product> обещает Product по любой строке: компилятор верит объявлению, а не содержимому объекта. Дыру закрывает флаг noUncheckedIndexedAccess из главы про настройку, но есть способ и без флагов — коллекция, у которой честный тип написан прямо в подписи.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Вторая дыра — as. Утверждение выключает проверку в одной точке, и достаточно перевесить идентификатор на обёртку, чтобы обещание перестало быть правдой.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Компилятор молчит: вы сказали ему, что это HTMLInputElement, — он поверил. В рендере такие утверждения заводятся чаще всего, потому что as HTMLInputElement — самый быстрый способ закрыть Element | null и не думать.

Третья дыра — сами данные. Типы описывают то, что вы обещали, а не то, что пришло: каталог с сервера остаётся unknown, пока его не разобрали проверками, — этим мы занимались в начале главы. К рендеру модель уже проверена, и Product[] здесь не предположение, а результат.

Строка конфигаЧто она даёт каталогуЦена
"strict": trueвосемь проверок разом: ни один узел и ни одна запись не трогаются без вопросаперенесённый код придётся дописать — это и есть работа этой статьи
"noUncheckedIndexedAccess": truebyId[id] и rows[0] начинают возвращать | undefinedпроверка в каждом месте, где раньше стоял просто индекс
"exactOptionalPropertyTypes": truetitle?: string перестаёт принимать явный undefinedчастичные обновления записи придётся собирать аккуратнее
"lib" — не написанаDOM подключается по умолчанию: document и Element видны самиуберёте DOM ради серверного кода — рендер встретит TS2584 «Не удается найти имя "document"»
"noEmit": true и скрипт tsc --noEmitпроверка типов — отдельная команда, видная в CI, а не побочный эффект сборкивыпуском кода занимается сборщик, и версии двух инструментов надо держать рядом

Редактор показывает ошибки только в открытых файлах — поэтому команда проверки в package.json нужна даже одиночке: она смотрит на весь проект сразу и говорит «зелено» одним ответом.

  • Карточка собирается из полей модели, поэтому опечатка в имени поля становится диагностикой, а не молчаливо ложной строкой на странице.
  • querySelector и closest возвращают | null, dataset — string | undefined: компилятор не читал вашу разметку и обязан сомневаться.
  • event.target — это EventTarget; путь от клика до записи каталога проходит через instanceof, проверку результата closest и find.
  • Диагностика не отменяет выполнение: пример сначала печатает претензию компилятора, а потом делает своё дело — или падает ровно там, куда он показывал.
  • strict на готовом коде даёт три класса ошибок: неявный any, возможные null и undefined, unknown в catch.
  • После strict остаются индексный доступ, as и непроверенные данные — их закрывают отдельные флаги и проверки на границе.

1/4В разметке есть <div id="catalog">, в коде — const root = document.querySelector("#catalog") и сразу root.append("текст"). Что произойдёт?

2/4Внутри обработчика клика написано event.target.closest("[data-id]"). Почему компилятор ругается на closest?

3/4card имеет тип HTMLElement. Какой тип у card.dataset.id?

4/4byId объявлен как Record<string, Product>, strict включён. Что будет с byId["p9"].title, если ключа p9 в объекте нет?

Рендер, не переживший строгость

Список рисуется правильно, но компилятор выдаёт две диагностики. Исправьте код так, чтобы их не стало, а список по-прежнему появлялся на странице. Разметка — <ul id="catalog"></ul>.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Первая диагностика — TS7006: у параметра product неоткуда взяться типу. Вторая — TS18047: querySelector отдаёт Element | null. Обе закрываются одной строкой каждая.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Проверка на null с throw лучше молчаливого ?.: если разметка изменится, вы узнаете об этом сразу, а не по пустому экрану. Тип HTMLLIElement у rowOf можно и не писать — компилятор выведет его из createElement("li").

Клик до записи

Обработчик должен печатать название товара, по карточке которого кликнули, и «клик мимо каталога» — если клик пришёлся не по карточке. Сейчас он даёт три диагностики и падает на запуске. Доведите путь от события до записи.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Диагностик три: event.target может быть null, у EventTarget нет dataset, а find возвращает Product | undefined. Каждая указывает на настоящую ступеньку: клик приходит по заголовку внутри карточки, и до data-id надо ещё подняться.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Обратите внимание: id здесь имеет тип string | undefined, и сравнение item.id === id это переживает — просто ни одна запись не совпадёт, и find вернёт undefined. Ветка «мимо каталога» появилась не для красоты.

Убрать as из рендера

Компилятор молчит, а код падает: as убедил его, что #search — это поле ввода, хотя идентификатор висит на обёртке. Перепишите так, чтобы тип брался из проверки, а не из обещания. Разметка — <div id="search"><input type="search" value=" кофе "></div>.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Искать надо само поле, а не обёртку, и подтверждать находку проверкой: instanceof сужает Element | null до HTMLInputElement честно — по настоящему прототипу, а не по слову автора.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Разница видна, если снова поменять разметку: вариант с as падает где-то посреди рендера с невразумительным TypeError, вариант с instanceof — сразу и с внятным текстом.