Menu

declare em TypeScript: arquivos de declaração .d.ts explicados

Um arquivo .d.ts descreve os tipos de um código JavaScript sem conter nada dele, e a palavra-chave declare faz o mesmo dentro de um arquivo .ts. Veja como arquivos de declaração são gerados, onde entram os pacotes @types, como tipar um módulo sem tipos e como declare global e module augmentation estendem tipos existentes.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Um arquivo de declaração (.d.ts) contém tipos e nada mais: assinaturas, interfaces e formatos de classes para um JavaScript que está em outro lugar. A palavra-chave declare faz o mesmo trabalho dentro de um arquivo comum. Ela diz ao compilador "isto existe em tempo de execução, pode confiar" e não gera código.

O compilador aceitou __APP_VERSION__.length porque a declaração diz que é uma string. Em tempo de execução essa variável não existe, então ler .length lança ReferenceError: __APP_VERSION__ is not defined, uma exceção que o compilador nunca previu. Esse é o contrato de toda declaração: os tipos só são verdadeiros na medida em que o JavaScript por trás deles é.

O que vai em um arquivo .d.ts

Compilar com declaration: true mostra bem a ideia. A partir deste código-fonte:

// price.ts
export interface LineItem {
    name: string;
    price: number;
    qty: number;
}

const TAX = 0.2;

export function total(items: LineItem[]) {
    const sum = items.reduce((acc, item) => acc + item.price * item.qty, 0);
    return Math.round(sum * (1 + TAX) * 100) / 100;
}

export class Cart {
    private items: LineItem[] = [];
    add(item: LineItem) {
        this.items.push(item);
        return this;
    }
}

o tsc gera price.js e este price.d.ts:

export interface LineItem {
    name: string;
    price: number;
    qty: number;
}
export declare function total(items: LineItem[]): number;
export declare class Cart {
    private items;
    add(item: LineItem): this;
}

Os corpos das funções somem, os tipos de retorno inferidos são escritos por extenso (number, this), o campo privado mantém o nome mas perde o tipo, e o TAX não exportado nem aparece. Uma biblioteca publica o .js para o Node e o .d.ts para o seu editor e o seu compilador. emitDeclarationOnly: true gera só os arquivos .d.ts, para projetos em que um bundler monta o JavaScript.

Os recursos nativos do ES2022 que você usa todo dia também vêm de arquivos de declaração: lib.es2022.d.ts e seus parentes vêm junto com o TypeScript e são escolhidos por target e lib.

As formas de declare

Toda instrução declare descreve algo que já existe. Em um arquivo .d.ts sem import nem export (um arquivo de declaração global), cada uma destas fica visível para o projeto inteiro:

// globals.d.ts
declare const API_URL: string;                     // a global constant
declare let debugMode: boolean;                    // a global variable
declare function track(event: string, props?: Record<string, string>): void;
declare class Widget {                             // a class from a script tag
    constructor(el: string);
    render(): void;
}
declare namespace Analytics {                      // a global object
    function page(name: string): void;
}
declare module "legacy-charts" {                   // a module you import
    export function draw(data: number[]): void;
}

Dentro de um arquivo .d.ts, interface e type não precisam de declare; qualquer outra declaração de nível superior sem declare ou export é o erro TS1046. Assim que um arquivo tem um import ou export, suas declarações passam a ser locais a ele, e acréscimos ao escopo global vão em um bloco declare global (mostrado abaixo). A assinatura declarada é o que o compilador usa para verificar as chamadas:

O compilador mostra index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'. Passe { items: "3" }, ou mude a declaração se a função real aceita números.

Pacotes @types

Muitos pacotes npm trazem seus próprios arquivos .d.ts, referenciados no campo types (ou em uma condição types dentro de exports) do seu package.json. Para pacotes só em JavaScript, o projeto DefinitelyTyped, mantido pela comunidade, publica tipos no escopo @types:

npm i lodash
npm i -D @types/lodash

Quando você faz import de um pacote, o TypeScript procura node_modules/@types/{name} automaticamente. Tipos que descrevem globais em vez de um import, como @types/node (process, Buffer) ou o describe e o it de um test runner, precisam estar listados no tsconfig.json:

{
    "compilerOptions": {
        "types": ["node"]
    }
}

Sem essa entrada, o TypeScript 7 não os carrega, mesmo com o pacote instalado:

error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

tsc --init escreve "types": [] na nova configuração, com um comentário sugerindo ["node"] para projetos Node.

Tipando um módulo sem tipos

Importar um pacote JavaScript que não tem tipos, nem incluídos nem em @types, é o erro TS7016:

error TS7016: Could not find a declaration file for module 'fakelib'. '/project/node_modules/fakelib/index.js' implicitly has an 'any' type.

Corrija com um arquivo .d.ts em qualquer lugar do projeto (uma pasta types/ é comum; ela só precisa estar coberta por include) sem import nem export no nível superior. Descreva as partes que você usa:

// types/fakelib.d.ts
declare module "fakelib" {
    export function hi(name: string): string;
    export const version: string;
}

// Non-code files a bundler lets you import
declare module "*.svg" {
    const url: string;
    export default url;
}

A versão mais curta, declare module "fakelib"; em uma linha, faz todo import do pacote ser any. Ela remove o erro e todas as verificações junto, então trate isso como um passo temporário.

declare global

Código que adiciona algo ao escopo global, como um polyfill, um novo método em um objeto nativo ou uma global definida por uma tag script, precisa dos tipos correspondentes. declare global os adiciona. Ele precisa ficar em um módulo (um arquivo com import ou export), por isso o export {} no topo:

interface Array<T> se mescla com a interface nativa Array em vez de substituí-la, e é var (não let nem const) que adiciona uma propriedade a globalThis. Estender protótipos nativos é arriscado em código compartilhado; a mesma técnica de declare global é como os projetos adicionam suas próprias variáveis a process.env na interface NodeJS.ProcessEnv do @types/node.

Module augmentation

Para acrescentar algo aos tipos de um pacote que você importa, declare de novo o nome do módulo e reabra a interface. O próprio arquivo precisa ser um módulo (o import cuida disso; export {} também funciona). Sem import nem export, o mesmo bloco declara um módulo config-lib totalmente novo, que esconde os tipos reais do pacote:

// types/config-lib.d.ts
import "config-lib";

declare module "config-lib" {
    interface Settings {
        beta: boolean; // merged into the package's own Settings interface
    }
}

Depois disso, load().beta de config-lib tem o tipo boolean em todo lugar. Interfaces se mesclam; type aliases não, então a biblioteca precisa exportar uma interface para isso funcionar. É assim que plugins acrescentam campos aos objetos de request ou de configuração de um framework.

skipLibCheck

skipLibCheck: true faz o compilador parar de verificar os tipos dos arquivos .d.ts, inclusive os de node_modules. Seu próprio código continua sendo verificado contra eles. Isso economiza tempo e evita erros de dois pacotes cujas declarações não batem, e é por isso que tsc --init a ativa. O custo é que um erro dentro dos seus próprios arquivos .d.ts também deixa de ser apontado.

Perguntas frequentes

O que é um arquivo .d.ts no TypeScript?

Um arquivo de declaração: ele contém só tipos (assinaturas de funções, interfaces, formatos de classes) e nenhuma implementação. Ele descreve um JavaScript que existe em outro lugar, como uma biblioteca compilada ou as APIs nativas do navegador, para que o TypeScript consiga verificar o código que o usa. O tsc nunca gera JavaScript para ele.

O que a palavra-chave declare faz no TypeScript?

Ela diz ao compilador que um valor existe em tempo de execução, sem criá-lo. declare const VERSION: string; permite usar VERSION como string, e a linha some da saída. Se nada realmente define VERSION, o programa falha em tempo de execução com um ReferenceError.

Como corrigir "Could not find a declaration file for module"?

O erro TS7016 significa que o pacote não tem tipos. Instale-os se existirem (npm i -D @types/package-name), ou adicione um arquivo .d.ts com declare module "package-name" { ... } descrevendo o que você usa. Só declare module "package-name"; silencia o erro tipando o módulo inteiro como any.

Como gerar arquivos .d.ts a partir de TypeScript?

Defina "declaration": true no tsconfig.json (ou passe --declaration). Cada arquivo .ts passa a gerar um .d.ts ao lado do seu .js. Adicione emitDeclarationOnly quando outra ferramenta gera o JavaScript, e declarationMap para que os editores consigam ir dos tipos ao seu código-fonte.

Qual é a diferença entre declare global e declare module?

declare global { ... } adiciona algo ao escopo global, por exemplo uma nova propriedade em Array ou uma variável global. declare module "name" { ... } descreve um módulo que você importa por esse nome. declare global precisa ficar em um módulo (um arquivo com import ou export); declare module declara um módulo novo em um arquivo sem imports nem exports, e estende um módulo existente quando está dentro de um módulo.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR