12k
All articles

Como corrigir erros do TypeScript em novas APIs do navegador

Corrija erros de TypeScript em novas APIs do navegador com modern-web-types ou uma declaração própria. Entenda o atraso dos tipos DOM e verifique o suporte em tempo de execução.

OpenReplay Team
OpenReplay Team
Como corrigir erros do TypeScript em novas APIs do navegador

O TypeScript exibe “Property does not exist on type” para algumas novas APIs do navegador porque seus tipos de DOM nativos incluem apenas APIs suportadas por pelo menos dois engines de navegador. Você pode resolver isso trocando por uma biblioteca de DOM mais completa ou declarando você mesmo o membro ausente.

Você chama uma API que o MDN documenta e que o Chrome já executa, e o compilador diz que ela não existe. Tipar como any faz o sublinhado vermelho sumir, mas você perde a checagem de tipos nesse trecho de código.

Este artigo explica por que os tipos de DOM do TypeScript ficam atrás dos navegadores e, em seguida, apresenta duas correções com segurança de tipos e a verificação em tempo de execução que você continua precisando em qualquer uma delas.

Principais pontos

  • O lib.dom.d.ts do TypeScript é gerado pelo microsoft/TypeScript-DOM-lib-generator, que deixa de fora qualquer API suportada por menos de dois dos principais engines de navegador.
  • O modern-web-types reduz essa exigência para um engine estável. Instale-o com npm install --save-dev @typescript/lib-dom@npm:modern-web-types.
  • No TypeScript 6 ou mais recente, o pacote só tem efeito com "libReplacement": true em compilerOptions. Do TypeScript 4.5 ao 5.x, nenhuma configuração extra é necessária.
  • Para adicionar você mesmo um membro ausente, use interface Window em um arquivo .d.ts global. Um alias type não faz merge.
  • Ter tipos para uma API não significa que o navegador a suporte, então APIs de um único engine ainda precisam de uma verificação como 'fetchLater' in window.

O que significa “Property does not exist on type”?

O erro TS2339 do TypeScript, “Property does not exist on type”, aparece quando seu código chama uma API real do navegador que os tipos de DOM embutidos no TypeScript não declaram. Seu código não adicionou uma variável global como window.myThing. O navegador fornece esse método, mas os tipos do TypeScript não o conhecem.

window.fetchLater('/analytics', { method: 'POST', activateAfter: 60_000 });
// error TS2339: Property 'fetchLater' does not exist on type 'Window & typeof globalThis'.

O fetchLater() enfileira uma requisição que o navegador envia quando o usuário sai da página (ela é descarregada ou vai para o bfcache) ou quando o timeout opcional activateAfter expira, o que acontecer primeiro. O anúncio do modern-web-types feito por Philip Walton também cita as view transitions com escopo de elemento e a propriedade scripts do Long Animation Frame como exemplos de APIs afetadas pelo mesmo erro.

Por que os tipos de DOM do TypeScript ficam atrás dos navegadores?

Os tipos de DOM do TypeScript ficam atrás dos navegadores de propósito. O README do TypeScript-DOM-lib-generator define como critério de inclusão o suporte em dois ou mais dos principais engines — Gecko (Firefox), Blink (Chrome/Edge) e WebKit (Safari) —, para que entrem apenas APIs com consenso real entre fornecedores. Todos os navegadores baseados em Chromium rodam sobre o Blink, então contam como um único engine.

Na prática, isso significa o seguinte: uma API lançada apenas no Blink pode funcionar no Chrome, no Edge e em qualquer outro navegador Chromium, e mesmo assim o TypeScript dirá que ela não existe. A entrada do web-features para fetchLater lista apenas navegadores baseados em Chromium como compatíveis, então ela não atende à regra dos dois engines.

O erro “Property does not exist on type” reflete a política de inclusão do TypeScript. Ele não indica se o navegador que executa seu código suporta a API.

Correção 1: substitua o lib.dom pelo modern-web-types

O modern-web-types é um pacote de Philip Walton que substitui as bibliotecas DOM e WebWorker do TypeScript sem exigir outras mudanças no seu código. Como explica o post de anúncio, ele é construído com o próprio gerador do TypeScript e com dados do w3c/webref, mas inclui qualquer API que já tenha sido lançada em um engine de navegador estável.

Instale-o sob o alias @typescript/lib-dom. Esse é o nome que o TypeScript procura ao resolver sua biblioteca de DOM:

npm install --save-dev @typescript/lib-dom@npm:modern-web-types

No TypeScript 6 ou mais recente, instalar o alias não tem efeito algum até que você também habilite o lib replacement:

{
  "compilerOptions": {
    "libReplacement": true
  }
}

Do TypeScript 4.5 ao 5.x, omita essa configuração. O TypeScript 4.5 introduziu a busca por @typescript/lib-* e a aplicava automaticamente. O TypeScript 5.8 colocou esse comportamento atrás da flag libReplacement, mas a manteve ativada por padrão, e o TypeScript 6 a desativou, como explica o pull request do modern-web-types que recomenda o lib replacement. Segundo o anúncio do TypeScript 7.0, a versão 7.0 adota os padrões da 6.0, incluindo libReplacement: false, então a 7.0 também precisa da flag.

Projetos com workers

Código de workers usa um entry point separado, adicionado via types. Deixe "WebWorker" fora de lib, porque o entry point o substitui:

{
  "compilerOptions": {
    "types": ["modern-web-types/webworker"],
    "lib": ["ESNext"]
  }
}

No TypeScript 5.x, definir types de qualquer forma impede que o TypeScript inclua automaticamente todos os pacotes @types/* presentes em node_modules. No TypeScript 6 e versões mais recentes, types já tem [] como padrão, então nada é incluído automaticamente de qualquer jeito. Em ambos os casos, se você depende de pacotes como @types/node, adicione-os ao mesmo array. O README também lista os entry points serviceworker, sharedworker e audioworklet.

Correção 2: declare a API ausente com interface merging

Se você só precisa de uma API, um pequeno arquivo de declaração resolve. Declare uma interface com o mesmo nome da interface nativa, e o TypeScript faz o merge das declarações. Precisa ser uma interface. Um alias type Window = { ... } falha com um erro de identificador duplicado, porque type aliases não fazem merge.

A assinatura abaixo segue o MDN: fetchLater() aceita os mesmos argumentos que fetch(), mais a opção activateAfter, e retorna um FetchLaterResult cujo booleano activated indica se a requisição já foi enviada.

// 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;
}

Isso funciona sem um wrapper declare global porque um arquivo .d.ts sem import ou export de nível superior é um script global. Três regras garantem que continue funcionando:

  • Mantenha o arquivo como não módulo. Se você adicionar um import ou export de nível superior, o arquivo vira um módulo. As interfaces deixam de fazer merge globalmente, e você voltaria a precisar de declare global.
  • Garanta que o tsconfig o inclua. O arquivo precisa corresponder aos seus padrões de include.
  • Apague-o quando o TypeScript passar a incluir a API. Assim que o lib.dom declarar os mesmos membros, suas cópias serão, na melhor das hipóteses, redundantes e, na pior, conflitantes.

Essa declaração tipa apenas window.fetchLater(...). Chamar a função global fetchLater(...) diretamente também exige uma declaração de função global, então use window.fetchLater no seu código.

Você ainda precisa de feature detection?

Sim. Os tipos do TypeScript descrevem como uma API se parece. Eles não dizem quais navegadores a possuem. As duas correções fazem o compilador aceitar fetchLater, mas navegadores que não a suportam ainda vão lançar um erro em tempo de execução, então proteja a chamada:

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
}

O bloco try está ali porque o MDN recomenda capturar QuotaExceededError em praticamente toda chamada a fetchLater(). Trate APIs de um único engine como progressive enhancement e verifique o suporte em relação aos navegadores-alvo do seu projeto na página de fetchLater do MDN.

Qual correção você deve usar?

AbordagemConfiguraçãoCoberturaQuando o TypeScript alcançar
modern-web-typesUma instalação, uma flag no TS 6+Todas as APIs de um único engineNada a remover
.d.ts escrito à mãoUm pequeno arquivo por APIApenas o que você escreverApague o arquivo
as any / @ts-ignoreNenhumaNenhumaEsconde erros reais indefinidamente

Use o modern-web-types se o seu projeto usa várias APIs novas ou adota com frequência recursos antes que cheguem a outros engines. Use um .d.ts escrito à mão se você precisa de um ou dois membros e prefere não substituir toda a biblioteca de DOM. Evite as any e @ts-ignore: eles desativam a checagem para toda a expressão ou linha e vão esconder erros genuínos no futuro. Impor isso com uma configuração do TypeScript ESLint e as regras @typescript-eslint/no-explicit-any e @typescript-eslint/ban-ts-comment evita que gambiarras avulsas se acumulem.

Conclusão

O erro “Property does not exist on type” do TypeScript para novas APIs do navegador vem da regra dos dois engines do TypeScript, e não de um erro no seu código ou de uma entrada "dom" ausente. Substitua o lib.dom pelo modern-web-types se você usa APIs novas com frequência, ou faça o merge de uma única interface em Window para um caso pontual, e mantenha uma verificação em tempo de execução em torno de cada chamada. Comece verificando sua versão do TypeScript: se for a 6 ou mais recente, adicione "libReplacement": true ao instalar o pacote.

Perguntas frequentes

Instalar @types/web resolve a falta de tipos para novas APIs do navegador?

Só para APIs que já atendem à regra dos dois engines. O @types/web é a mesma biblioteca de DOM que vem embutida no TypeScript. Ele é gerado pelo microsoft/TypeScript-DOM-lib-generator e publicado no npm, então pode entregar tipos de DOM mais novos antes do próximo release do TypeScript. Ele aplica a mesma política de inclusão, então uma API exclusiva do Chromium como fetchLater continua ausente. Ele é instalado pelo mesmo alias @typescript/lib-dom no TypeScript 4.5 e versões posteriores e, no TypeScript 6 ou mais recente, também precisa de libReplacement definido como true.

Adicionar DOM ou ESNext à opção lib resolve o erro?

Não. A opção lib apenas escolhe quais arquivos de declaração embutidos no TypeScript são carregados. O arquivo de DOM embutido é gerado sob a regra dos dois engines, então adicionar 'DOM', 'DOM.Iterable' ou 'ESNext' não pode incluir uma API que esse arquivo deixa de fora. O ESNext adiciona recursos da linguagem JavaScript, não APIs do navegador. Para corrigir o erro, substitua a biblioteca de DOM pelo modern-web-types ou declare o membro ausente em um arquivo .d.ts global.

Como declarar um método ausente em Element ou HTMLElement em vez de Window?

Faça o merge na interface em que a especificação define o membro. Em um arquivo .d.ts global sem import ou export de nível superior, declare interface Element com o novo método. HTMLElement, SVGElement e as demais interfaces de elementos estendem Element, então todas o herdam. O startViewTransition() com escopo de elemento é definido em Element, então declará-lo apenas em HTMLElement deixa os elementos SVG sem tipagem. Pegue a assinatura exata no MDN ou na especificação.

Open-source session replay

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

We use cookies to improve your experience. By using our site, you accept cookies.