Declaração de tipos para bibliotecas JS: arquivos .d.ts

1. Introdução aos arquivos .d.ts

Quando trabalhamos com TypeScript, um dos maiores desafios é integrar bibliotecas JavaScript que não foram escritas originalmente em TypeScript. É aqui que entram os arquivos de declaração de tipos, conhecidos como .d.ts. Esses arquivos funcionam como uma "camada de descrição" que informa ao TypeScript quais são as formas, tipos e assinaturas das funções, variáveis e classes expostas por uma biblioteca JS pura.

A diferença fundamental entre um arquivo .ts e um .d.ts é que o primeiro contém implementação real (código executável), enquanto o segundo contém apenas declarações — ou seja, descreve a estrutura sem implementar nada. O TypeScript usa esses arquivos para realizar verificações de tipo em tempo de compilação, sem gerar código JavaScript a partir deles.

No ecossistema TypeScript, os arquivos .d.ts são essenciais para dar vida à tipagem de bibliotecas como jQuery, Lodash, Moment.js e milhares de outras que não foram migradas para TypeScript.

2. Estrutura básica de um arquivo de declaração

A sintaxe fundamental dos arquivos .d.ts gira em torno de três palavras-chave principais: declare, module e namespace. O declare é usado para indicar que algo existe em tempo de execução, mas não precisa ser implementado no arquivo de declaração.

Vamos começar com um exemplo simples de declaração para uma biblioteca hipotética chamada math-utils:

// math-utils.d.ts
declare function add(a: number, b: number): number;
declare function subtract(a: number, b: number): number;
declare const PI: number;

Aqui declaramos duas funções e uma constante global. Note que não há corpo de função — apenas a assinatura. Isso é suficiente para que o TypeScript saiba como usar essas funções.

Se a biblioteca expõe classes, podemos declará-las assim:

declare class Calculator {
  constructor(initialValue: number);
  add(value: number): number;
  getValue(): number;
}

3. Declarando módulos e namespaces

Para bibliotecas que utilizam sistemas de módulos (CommonJS, AMD, ES modules), usamos declare module para mapear o nome do módulo:

// types/math-utils.d.ts
declare module "math-utils" {
  export function add(a: number, b: number): number;
  export function multiply(a: number, b: number): number;
  export const VERSION: string;
}

Quando a biblioteca expõe um namespace global que também pode ser importado como módulo, usamos namespace:

declare namespace MathUtils {
  function add(a: number, b: number): number;
  function multiply(a: number, b: number): number;
  namespace Advanced {
    function power(base: number, exp: number): number;
  }
}

Para bibliotecas que funcionam tanto como namespace global quanto como módulo (padrão UMD), combinamos ambas as abordagens:

declare module "math-utils" {
  export = MathUtils;
}

declare namespace MathUtils {
  function add(a: number, b: number): number;
}

4. Tipos avançados em declarações

Declarações de tipos complexos são comuns em bibliotecas reais. Vamos ver como declarar interfaces, tipos união e genéricos:

declare module "data-processor" {
  interface ProcessConfig {
    encoding?: "utf8" | "ascii" | "base64";
    maxSize?: number;
    transform?: (data: string) => string;
  }

  type DataSource = string | Buffer | File;

  function process<T>(data: T, config?: ProcessConfig): Promise<T>;

  class StreamProcessor<T> {
    constructor(source: DataSource);
    pipe<R>(transform: (data: T) => R): StreamProcessor<R>;
    start(): Promise<T[]>;
  }
}

Para compatibilidade com diferentes sistemas de módulos, usamos export = e export default:

declare module "legacy-lib" {
  // Para módulos CommonJS que exportam um único valor
  function legacyFunction(x: number): string;
  export = legacyFunction;
}

declare module "esm-lib" {
  // Para módulos ES que usam export default
  interface Options {
    debug?: boolean;
  }
  export default function main(options?: Options): void;
}

Declaração de sobrecargas de função:

declare function format(input: string): string;
declare function format(input: number): string;
declare function format(input: Date, locale?: string): string;

5. Trabalhando com bibliotecas globais e UMD

Bibliotecas globais expõem suas funcionalidades diretamente no escopo global (como window.$ no jQuery). Para declará-las:

// jquery.d.ts
declare var $: JQueryStatic;

interface JQueryStatic {
  (selector: string): JQuery;
  (element: HTMLElement): JQuery;
  ajax(settings: JQueryAjaxSettings): JQueryXHR;
}

interface JQuery {
  addClass(className: string): this;
  removeClass(className: string): this;
  html(content?: string): string | this;
}

Para bibliotecas UMD que funcionam tanto como global quanto como módulo, usamos export as namespace:

declare module "my-umd-lib" {
  export function doSomething(): void;
  export as namespace MyUmdLib;
}

Isso permite que a biblioteca seja usada tanto com import { doSomething } from "my-umd-lib" quanto como MyUmdLib.doSomething() no escopo global.

6. Geração e manutenção de arquivos .d.ts

O TypeScript pode gerar arquivos .d.ts automaticamente a partir de código TypeScript. Basta configurar o tsconfig.json:

{
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "./types",
    "emitDeclarationOnly": true
  }
}

Com declaration: true, o compilador gera arquivos .d.ts para cada arquivo .ts. A opção emitDeclarationOnly é útil quando você quer apenas os tipos, sem emitir JavaScript.

A diferença entre arquivos gerados e escritos manualmente é significativa:
- Gerados: refletem exatamente a implementação, mas podem expor detalhes internos
- Manuais: permitem controle sobre o que é exposto, ideal para bibliotecas JS legadas

Boas práticas para manutenção:

// Teste de tipos com @ts-expect-error
import { add } from "math-utils";

// @ts-expect-error — deve falhar porque espera números
add("1", "2");

Versionamento: sempre alinhe a versão do pacote de tipos com a versão da biblioteca. Use @ts-expect-error em testes para garantir que tipos incorretos sejam rejeitados.

7. Publicação e distribuição no ecossistema

Para publicar seus próprios tipos, configure o package.json:

{
  "name": "math-utils",
  "version": "1.0.0",
  "types": "./types/index.d.ts",
  "typings": "./types/index.d.ts"
}

O campo types (ou typings para versões antigas) aponta para o arquivo de declaração principal.

Se a biblioteca não tem tipos próprios, você pode contribuir com o repositório DefinitelyTyped (publicado como @types/* no npm). Por exemplo, @types/jquery, @types/lodash.

Para evitar conflitos de versão:
- Publique tipos junto com a biblioteca sempre que possível
- Se usar @types/*, especifique a versão compatível no package.json
- Teste a compatibilidade com npm ls @types/minha-biblioteca

Dicas finais:
- Prefira interface a type para objetos que podem ser estendidos
- Use readonly para propriedades imutáveis
- Documente parâmetros complexos com JSDoc nos arquivos .d.ts

Referências