Как исправить ошибки TypeScript для новых браузерных API
Исправьте ошибки TypeScript для новых API браузера с помощью modern-web-types или собственной декларации. Узнайте, почему типы DOM отстают и как проверять поддержку во время выполнения.
TypeScript выдаёт ошибку «Property does not exist on type» для некоторых новых браузерных API, потому что встроенные типы DOM включают только API, которые поддерживаются как минимум двумя браузерными движками. Решить проблему можно двумя способами: подключить более полную библиотеку типов DOM или самостоятельно объявить недостающий член.
Вы вызываете API, который описан на MDN и уже работает в Chrome, а компилятор сообщает, что такого API не существует. Если указать тип any, красное подчёркивание исчезнет, но этот код останется без проверки типов.
В статье объясняется, почему типы DOM в TypeScript отстают от браузеров. Затем мы рассмотрим два типобезопасных способа исправить ошибку и проверку в рантайме, которая нужна при любом из них.
Ключевые выводы
- Файл lib.dom.d.ts в TypeScript генерируется инструментом microsoft/TypeScript-DOM-lib-generator, который отбрасывает любой API, поддерживаемый менее чем двумя основными браузерными движками.
- modern-web-types снижает эту планку до одного стабильного движка. Установка:
npm install --save-dev @typescript/lib-dom@npm:modern-web-types. - В TypeScript 6 и новее пакет начинает работать только при наличии
"libReplacement": trueв compilerOptions. Для TypeScript с 4.5 по 5.x дополнительная настройка не нужна. - Чтобы добавить один недостающий член самостоятельно, используйте
interface Windowв глобальном файле.d.ts. Псевдонимtypeне сливается с существующим объявлением. - Наличие типов для API ещё не означает, что браузер его поддерживает, поэтому для API, реализованных только в одном движке, по-прежнему нужна проверка вида
'fetchLater' in window.
Что означает ошибка «Property does not exist on type»?
Ошибка TypeScript TS2339 «Property does not exist on type» возникает, когда ваш код вызывает реальный браузерный API, который не объявлен во встроенных типах DOM. Речь не о глобальной переменной, которую ваш код добавил сам, вроде window.myThing. Метод предоставляет браузер, но типы TypeScript о нём ничего не знают.
window.fetchLater('/analytics', { method: 'POST', activateAfter: 60_000 });
// error TS2339: Property 'fetchLater' does not exist on type 'Window & typeof globalThis'.
fetchLater() ставит в очередь запрос, который браузер отправит, когда пользователь покинет страницу (она выгрузится или попадёт в bfcache), либо по истечении необязательного тайм-аута activateAfter, если он наступит раньше. В анонсе modern-web-types Филип Уолтон (Philip Walton) приводит и другие примеры API, на которых возникает та же ошибка: view transitions на уровне отдельных элементов и свойство scripts из Long Animation Frame.
Почему типы DOM в TypeScript отстают от браузеров?
Типы DOM в TypeScript отстают от браузеров намеренно. В README TypeScript-DOM-lib-generator установлен критерий включения: поддержка как минимум в двух из основных движков — Gecko (Firefox), Blink (Chrome/Edge) и WebKit (Safari). Так в типы попадают только API, о которых действительно договорились разные производители. Все браузеры на базе Chromium работают на Blink, поэтому считаются одним движком.
На практике это означает следующее: API, реализованный только в Blink, может работать в Chrome, Edge и любом другом браузере на Chromium, а TypeScript всё равно будет утверждать, что его не существует. Согласно записи web-features для fetchLater, его поддерживают только браузеры на базе Chromium, поэтому правило двух движков он не проходит.
Ошибка «Property does not exist on type» отражает политику включения API в TypeScript. Она ничего не говорит о том, поддерживает ли этот API браузер, в котором выполняется ваш код.
Способ 1: замените lib.dom на modern-web-types
modern-web-types — пакет Филипа Уолтона, который подменяет библиотеки DOM и WebWorker в TypeScript без каких-либо других изменений в вашем коде. Как поясняется в его анонсе, пакет собирается тем же генератором, что и в TypeScript, на основе данных w3c/webref, но включает любой API, который вышел хотя бы в одном стабильном браузерном движке.
Установите его под псевдонимом @typescript/lib-dom — именно это имя TypeScript ищет при разрешении библиотеки DOM:
npm install --save-dev @typescript/lib-dom@npm:modern-web-types
В TypeScript 6 и новее установка псевдонима ни на что не повлияет, пока вы дополнительно не включите замену библиотек (lib replacement):
{
"compilerOptions": {
"libReplacement": true
}
}
В TypeScript с 4.5 по 5.x эту настройку указывать не нужно. В TypeScript 4.5 появился поиск пакетов @typescript/lib-*, и он применялся автоматически. В TypeScript 5.8 его перевели под флаг libReplacement, но оставили флаг включённым по умолчанию, а в TypeScript 6 его отключили — об этом рассказывается в pull request в modern-web-types с рекомендацией использовать lib replacement. Согласно анонсу TypeScript 7.0, версия 7.0 перенимает значения по умолчанию из 6.0, включая libReplacement: false, поэтому в 7.0 флаг тоже нужен.
Проекты с воркерами
Для кода воркеров предусмотрена отдельная точка входа, которая подключается через types. Не указывайте "WebWorker" в lib, потому что эта точка входа его заменяет:
{
"compilerOptions": {
"types": ["modern-web-types/webworker"],
"lib": ["ESNext"]
}
}
В TypeScript 5.x само по себе задание types отключает автоматическое подключение всех пакетов @types/* из node_modules. В TypeScript 6 и новее types по умолчанию и так равен [], поэтому автоматически ничего не подключается в любом случае. В обоих случаях, если вы используете пакеты вроде @types/node, добавьте их в тот же массив. В README также перечислены точки входа serviceworker, sharedworker и audioworklet.
Способ 2: объявите недостающий API через слияние интерфейсов
Если вам нужен только один API, достаточно небольшого файла объявлений. Объявите интерфейс с тем же именем, что и встроенный, и TypeScript объединит объявления. Это обязательно должен быть interface. Псевдоним type Window = { ... } приведёт к ошибке дублирования идентификатора, поскольку псевдонимы типов не сливаются.
Приведённая ниже сигнатура соответствует MDN: fetchLater() принимает те же аргументы, что и fetch(), плюс параметр activateAfter, и возвращает объект FetchLaterResult, булево свойство activated которого показывает, был ли запрос уже отправлен.
// src/types/fetch-later.d.ts
interface DeferredRequestInit extends RequestInit {
activateAfter?: number;
}
interface FetchLaterResult {
readonly activated: boolean;
}
interface Window {
fetchLater(input: RequestInfo | URL, init?: DeferredRequestInit): FetchLaterResult;
}
Обёртка declare global здесь не нужна, потому что файл .d.ts без import или export верхнего уровня является глобальным скриптом. Чтобы всё работало, соблюдайте три правила:
- Не превращайте файл в модуль. Если добавить
importилиexportверхнего уровня, файл станет модулем. Интерфейсы перестанут сливаться глобально, и снова понадобитсяdeclare global. - Убедитесь, что файл охвачен tsconfig. Он должен соответствовать шаблонам в
include. - Удалите файл, когда API появится в TypeScript. Как только в lib.dom будут объявлены те же члены, ваши копии в лучшем случае станут избыточными, а в худшем — начнут конфликтовать.
Это объявление типизирует только window.fetchLater(...). Для вызова глобальной функции fetchLater(...) без префикса потребуется ещё и объявление глобальной функции, поэтому в коде используйте window.fetchLater.
Нужна ли по-прежнему проверка поддержки?
Да. Типы TypeScript описывают, как выглядит API, но не говорят, в каких браузерах он есть. Оба способа заставляют компилятор принять fetchLater, но в браузерах без его поддержки код всё равно упадёт в рантайме, поэтому защитите вызов проверкой:
if ('fetchLater' in window) {
try {
window.fetchLater('/analytics', { method: 'POST', activateAfter: 60_000 });
} catch {
// quota exceeded: fall back or drop the beacon
}
} else {
// fall back, e.g. send the data immediately
}
Блок try нужен потому, что MDN рекомендует перехватывать QuotaExceededError практически при каждом вызове fetchLater(). Используйте API, реализованные только в одном движке, в рамках прогрессивного улучшения (progressive enhancement) и сверяйте их поддержку с целевыми браузерами вашего проекта на странице fetchLater в MDN.
Какой способ выбрать?
| Подход | Настройка | Покрытие | Когда TypeScript догонит браузеры |
|---|---|---|---|
| modern-web-types | Одна установка, один флаг в TS 6+ | Все API, реализованные хотя бы в одном движке | Ничего удалять не нужно |
Собственный .d.ts | Один небольшой файл на каждый API | Только то, что вы объявили | Удалить файл |
as any / @ts-ignore | Не требуется | Отсутствует | Бессрочно скрывает реальные ошибки |
Выбирайте modern-web-types, если в проекте используется несколько новых API или вы регулярно внедряете возможности до того, как они появятся в других движках. Собственный .d.ts подойдёт, если вам нужны один-два члена и вы не хотите заменять всю библиотеку DOM. Избегайте as any и @ts-ignore: они отключают проверку для всего выражения или строки и в будущем скроют настоящие ошибки. Закрепить этот подход поможет настройка TypeScript ESLint с правилами @typescript-eslint/no-explicit-any и @typescript-eslint/ban-ts-comment — они не дадут случайным обходным решениям накапливаться в кодовой базе.
Заключение
Ошибка TypeScript «Property does not exist on type» для новых браузерных API вызвана правилом двух движков, а не ошибкой в вашем коде или отсутствием "dom" в настройках. Если вы регулярно используете новые API, замените lib.dom на modern-web-types; для разового случая достаточно слить один интерфейс с Window. В обоих случаях сопровождайте каждый вызов проверкой в рантайме. Начните с проверки версии TypeScript: если у вас версия 6 или новее, при установке пакета добавьте "libReplacement": true.
Часто задаваемые вопросы
Поможет ли установка @types/web с отсутствующими типами для новых браузерных API?
Только для API, которые уже удовлетворяют правилу двух движков. @types/web — это та же библиотека DOM, что поставляется в составе TypeScript. Она генерируется инструментом microsoft/TypeScript-DOM-lib-generator и публикуется в npm, поэтому может предоставлять более свежие типы DOM до выхода следующего релиза TypeScript. Однако в ней действует та же политика включения, так что API, доступный только в Chromium, например fetchLater, в ней по-прежнему отсутствует. Пакет устанавливается через тот же псевдоним @typescript/lib-dom в TypeScript 4.5 и новее, а в TypeScript 6 и новее для него также нужно установить libReplacement в значение true.
Исправит ли ошибку добавление DOM или ESNext в параметр lib?
Нет. Параметр lib лишь определяет, какие из встроенных файлов объявлений TypeScript будут загружены. Встроенный файл DOM генерируется по правилу двух движков, поэтому добавление 'DOM', 'DOM.Iterable' или 'ESNext' не может добавить API, которого в этом файле нет. ESNext добавляет возможности языка JavaScript, а не браузерные API. Чтобы исправить ошибку, замените библиотеку DOM на modern-web-types или объявите недостающий член в глобальном файле .d.ts.
Как объявить недостающий метод для Element или HTMLElement, а не для Window?
Выполните слияние с тем интерфейсом, в котором этот член определён спецификацией. В глобальном файле .d.ts без import или export верхнего уровня объявите interface Element с новым методом. HTMLElement, SVGElement и другие интерфейсы элементов расширяют Element, поэтому все они его унаследуют. Метод startViewTransition() на уровне элемента определён в Element, поэтому если объявить его только в HTMLElement, SVG-элементы останутся без типов. Точную сигнатуру берите из MDN или спецификации.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k