Рендер и доводка настроек
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Модель наконец встречается с экраном. DOM — вторая граница программы: он отвечает так же уклончиво, как чужой JSON, только вместо unknown здесь Element | null, string | undefined и EventTarget. Пройдём каталог от записи до узла и обратно, а потом затянем конфиг и разберём каждую всплывшую диагностику.
Доска задач из части II перерисовывалась целиком из массива: данные решают, разметка отражает. Каталог устроен так же, но карточка теперь собирается из полей Product, и каждое имя на этом пути проверено.
Тип HTMLElement у cardOf написан для читателя — компилятор вывел бы его сам. А вот createElement знает больше, чем кажется: createElement("input") отдаёт HTMLInputElement со всеми его полями, а createElement("article") — просто HTMLElement, потому что своего интерфейса у этого тега нет.
Проверка имён — не абстрактная польза. Опечатка в поле не ломает страницу: карточка соберётся и будет спокойно врать.
product.stock — это undefined, undefined ложно, и товар в наличии показан как отсутствующий. Компилятор находит это до запуска и даже подсказывает верное имя: «Свойство "stock" не существует в типе "Product". Вы имели в виду "inStock"?».
Соберите ту же карточку строкой через innerHTML — и проверка исчезнет: внутри кавычек компилятор не читает ничего, там снова просто текст.
querySelector объявлен как Element | null — и это первая настоящая ссора с DOM: вы-то знаете, что #catalog в разметке есть.
Две диагностики — и работающая страница. Выполнение они не отменили: типы стираются, и tsc по умолчанию выпускает код даже с ошибками. Но придирки здесь нет. Разметку компилятор не читал и читать не может: поменяется вёрстка, переименуется идентификатор, шаблон соберётся по-другому — и null станет реальностью. С ?. такая опечатка даже не упадёт, а молча ничего не нарисует.
Поэтому узел, без которого рендер бессмыслен, разумно требовать громко — один раз, на старте.
Тип E у mustFind выбирает вызывающий, и никто его не проверяет — та же ловушка дженериков, что в главе про них: подпись удобная, а ответственность на вас.
У querySelector есть перегрузка по имени тега: для строки "input" она отдаёт HTMLInputElement | null. Но разбирать селектор целиком компилятор не берётся.
Отсюда практический вывод: если нужно именно value, ищите по тегу и уточняйте поиск контекстом — form.querySelector("input") вместо document.querySelector("input#search"). Либо проверяйте instanceof HTMLInputElement: в отличие от as, это настоящая проверка, а не обещание.
Обратный путь начинается с делегирования — одного обработчика на контейнере, как на доске задач в части II. Только теперь между кликом и записью каталога стоят три типа, и каждый требует ответа.
«Свойство "closest" не существует в типе "EventTarget"» звучит странно ровно до того момента, как вспомнишь: target — не элемент. Событие приходит и от window, и от XMLHttpRequest, и от любого объекта, умеющего принимать события; общий контракт EventTarget знает только про подписку и отправку.
Ступеней ровно три, и ни одна не лишняя. instanceof HTMLElement сужает EventTarget | null до элемента — тот же приём, что в главе про сужение. closest отдаёт Element | null: карточки над кликом может не быть вовсе. А dataset — это DOMStringMap с индексной сигнатурой, поэтому card.dataset.id имеет тип string | undefined, и find следом честно возвращает Product | undefined.
closest<HTMLElement> — утверждение в маске дженерика: компилятор поверит на слово. Если карточкой может оказаться что угодно, надёжнее ещё один instanceof.
Пока каталог писался с нуля, строгость не мешала: код рождался уже с типами. Но в проект приезжает чужое — функции из части II, наброски из консоли, кусок соседнего модуля. Строка "strict": true включает восемь проверок разом (в главе про настройку они разобраны по одной), и на готовом коде всплывают три класса диагностик.
Первый класс — неявный any. Функция работает и печатает что надо; компилятор всего лишь отказывается угадывать, что ей передадут. Одна аннотация product: Product убирает диагностику и заодно включает проверку полей внутри функции.
Второй класс — то, чего может не быть. find объявлен как Product | undefined, и это не формальность: товара p9 в каталоге нет, и на запуске код падает ровно там, куда показал компилятор. Такой пример полезно досмотреть до конца: сначала диагностика, потом TypeError — одна ошибка, пойманная дважды.
В catch попадает что угодно: throw "строка" никто не запрещал. Поэтому строгость объявляет переменную ошибки как unknown, и обращение к .message становится диагностикой — той же, что в статье про unknown на границе программы.
Текст сообщения у каждого движка свой, а вот лечение общее: спросить, что поймали, вместо того чтобы утверждать.
| Диагностика | Откуда она в каталоге | Чем закрываем |
|---|---|---|
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 следом — он даёт самый длинный список и самую большую пользу.
Строгость — не потолок. В каталоге после неё остаются три дыры, и все три видны на запуске.
Record<string, Product> обещает Product по любой строке: компилятор верит объявлению, а не содержимому объекта. Дыру закрывает флаг noUncheckedIndexedAccess из главы про настройку, но есть способ и без флагов — коллекция, у которой честный тип написан прямо в подписи.
Вторая дыра — as. Утверждение выключает проверку в одной точке, и достаточно перевесить идентификатор на обёртку, чтобы обещание перестало быть правдой.
Компилятор молчит: вы сказали ему, что это HTMLInputElement, — он поверил. В рендере такие утверждения заводятся чаще всего, потому что as HTMLInputElement — самый быстрый способ закрыть Element | null и не думать.
Третья дыра — сами данные. Типы описывают то, что вы обещали, а не то, что пришло: каталог с сервера остаётся unknown, пока его не разобрали проверками, — этим мы занимались в начале главы. К рендеру модель уже проверена, и Product[] здесь не предположение, а результат.
| Строка конфига | Что она даёт каталогу | Цена |
|---|---|---|
"strict": true | восемь проверок разом: ни один узел и ни одна запись не трогаются без вопроса | перенесённый код придётся дописать — это и есть работа этой статьи |
"noUncheckedIndexedAccess": true | byId[id] и rows[0] начинают возвращать | undefined | проверка в каждом месте, где раньше стоял просто индекс |
"exactOptionalPropertyTypes": true | title?: 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>.
Первая диагностика — TS7006: у параметра product неоткуда взяться типу. Вторая — TS18047: querySelector отдаёт Element | null. Обе закрываются одной строкой каждая.
Проверка на null с throw лучше молчаливого ?.: если разметка изменится, вы узнаете об этом сразу, а не по пустому экрану. Тип HTMLLIElement у rowOf можно и не писать — компилятор выведет его из createElement("li").
Клик до записи
Обработчик должен печатать название товара, по карточке которого кликнули, и «клик мимо каталога» — если клик пришёлся не по карточке. Сейчас он даёт три диагностики и падает на запуске. Доведите путь от события до записи.
Диагностик три: event.target может быть null, у EventTarget нет dataset, а find возвращает Product | undefined. Каждая указывает на настоящую ступеньку: клик приходит по заголовку внутри карточки, и до data-id надо ещё подняться.
Обратите внимание: id здесь имеет тип string | undefined, и сравнение item.id === id это переживает — просто ни одна запись не совпадёт, и find вернёт undefined. Ветка «мимо каталога» появилась не для красоты.
Убрать as из рендера
Компилятор молчит, а код падает: as убедил его, что #search — это поле ввода, хотя идентификатор висит на обёртке. Перепишите так, чтобы тип брался из проверки, а не из обещания. Разметка — <div id="search"><input type="search" value=" кофе "></div>.
Искать надо само поле, а не обёртку, и подтверждать находку проверкой: instanceof сужает Element | null до HTMLInputElement честно — по настоящему прототипу, а не по слову автора.
Разница видна, если снова поменять разметку: вариант с as падает где-то посреди рендера с невразумительным TypeError, вариант с instanceof — сразу и с внятным текстом.