TypeScript

Boas Práticas de Escrevendo Arquivos .d.ts: Tipando Bibliotecas JavaScript Existentes para Times Ágeis

12 min de leitura

Boas Práticas de Escrevendo Arquivos .d.ts: Tipando Bibliotecas JavaScript Existentes para Times Ágeis

O que são Arquivos .d.ts e Por Que Importam Os arquivos (TypeScript Declaration Files) são documentos especiais que descrevem a estrutura de tipos de código JavaScript. Quando você trabalha com uma biblioteca JavaScript sem suporte nativo a TypeScript, o arquivo atua como um intermediário que fornece informações de tipo ao seu editor e ao compilador TypeScript, permitindo autocompletar, verificação de tipos e documentação contextual. Imagine que você usa uma biblioteca JavaScript clássica em um projeto TypeScript. Sem tipos, o TypeScript a tratará como , perdendo todos os benefícios de segurança de tipos. O arquivo resolve isso descrevendo, em linguagem TypeScript, quais funções existem, quais parâmetros elas aceitam e o que retornam. É como fornecer um "contrato" que a biblioteca JavaScript cumpre, sem modificar o código JavaScript original. Estrutura Básica de um Arquivo .d.ts Sintaxe Fundamental Um arquivo usa a mesma sintaxe que arquivos normais, mas contém apenas declarações de tipo, não implementação. Vamos começar com um exemplo simples. Suponha que

<h2>O que são Arquivos .d.ts e Por Que Importam</h2>

<p>Os arquivos <code>.d.ts</code> (TypeScript Declaration Files) são documentos especiais que descrevem a estrutura de tipos de código JavaScript. Quando você trabalha com uma biblioteca JavaScript sem suporte nativo a TypeScript, o arquivo <code>.d.ts</code> atua como um intermediário que fornece informações de tipo ao seu editor e ao compilador TypeScript, permitindo autocompletar, verificação de tipos e documentação contextual.</p>

<p>Imagine que você usa uma biblioteca JavaScript clássica em um projeto TypeScript. Sem tipos, o TypeScript a tratará como <code>any</code>, perdendo todos os benefícios de segurança de tipos. O arquivo <code>.d.ts</code> resolve isso descrevendo, em linguagem TypeScript, quais funções existem, quais parâmetros elas aceitam e o que retornam. É como fornecer um &quot;contrato&quot; que a biblioteca JavaScript cumpre, sem modificar o código JavaScript original.</p>

<h2>Estrutura Básica de um Arquivo .d.ts</h2>

<h3>Sintaxe Fundamental</h3>

<p>Um arquivo <code>.d.ts</code> usa a mesma sintaxe que arquivos <code>.ts</code> normais, mas contém apenas declarações de tipo, não implementação. Vamos começar com um exemplo simples. Suponha que você tenha uma biblioteca JavaScript chamada <code>math-helper.js</code>:</p>

<pre><code class="language-javascript">// math-helper.js (JavaScript puro)

function add(a, b) {

return a + b;

}

function multiply(a, b) {

return a * b;

}

module.exports = { add, multiply };</code></pre>

<p>O arquivo <code>.d.ts</code> correspondente seria:</p>

<pre><code class="language-typescript">// math-helper.d.ts

export function add(a: number, b: number): number;

export function multiply(a: number, b: number): number;</code></pre>

<p>A diferença crucial é que no <code>.d.ts</code> você <em>declara</em> tipos sem implementar a lógica. Use <code>export</code> para funções que serão acessíveis externamente, e sempre especifique tipos de parâmetros e retorno.</p>

<h3>Declarando Interfaces e Tipos</h3>

<p>Quando sua biblioteca trabalha com objetos complexos, você precisa descrever sua estrutura. Use <code>interface</code> para contratos e <code>type</code> para aliases. Aqui está um exemplo mais realista:</p>

<pre><code class="language-typescript">// user-service.d.ts

export interface User {

id: number;

name: string;

email: string;

isActive?: boolean; // propriedade opcional

}

export interface CreateUserRequest {

name: string;

email: string;

}

export function getUser(id: number): Promise&lt;User&gt;;

export function createUser(data: CreateUserRequest): Promise&lt;User&gt;;

export function deleteUser(id: number): Promise&lt;void&gt;;</code></pre>

<p>Note que a propriedade <code>isActive</code> tem <code>?</code>, indicando que é opcional. Este padrão permite que quem usar a biblioteca saiba exatamente qual estrutura esperar, sem ler a documentação manualmente.</p>

<h2>Tipando Padrões Comuns de Bibliotecas JavaScript</h2>

<h3>Classes e Construtores</h3>

<p>Muitas bibliotecas JavaScript expõem classes. No <code>.d.ts</code>, declare usando <code>declare class</code>:</p>

<pre><code class="language-typescript">// event-emitter.d.ts

declare class EventEmitter {

constructor();

on(eventName: string, callback: (data: any) =&gt; void): void;

emit(eventName: string, data?: any): void;

off(eventName: string, callback: (data: any) =&gt; void): void;

}

export = EventEmitter;</code></pre>

<p>Neste exemplo, usamos <code>export =</code> (sintaxe CommonJS) porque a biblioteca original usa <code>module.exports</code>. Se fosse ES6, seria <code>export default EventEmitter</code> ou <code>export { EventEmitter }</code>.</p>

<h3>Callbacks e Funções de Ordem Superior</h3>

<p>JavaScript frequentemente passa funções como argumentos. Descreva usando tipos de função:</p>

<pre><code class="language-typescript">// request-helper.d.ts

export type RequestCallback = (error: Error | null, data?: any) =&gt; void;

export function fetchData(

url: string,

options?: RequestOptions,

callback?: RequestCallback

): void;

export interface RequestOptions {

method?: &#039;GET&#039; | &#039;POST&#039; | &#039;PUT&#039; | &#039;DELETE&#039;;

headers?: Record&lt;string, string&gt;;

timeout?: number;

}</code></pre>

<p>O tipo <code>RequestCallback</code> descreve uma função que recebe um erro opcional e dados opcionais. Use <code>Record&lt;string, string&gt;</code> para descrever objetos chave-valor quando a estrutura é dinâmica.</p>

<h3>Genéricos</h3>

<p>Se a biblioteca usa tipos genéricos (como contêineres que aceitam qualquer tipo), descreva usando <code>&lt;T&gt;</code>:</p>

<pre><code class="language-typescript">// storage.d.ts

export interface Storage&lt;T&gt; {

get(key: string): T | undefined;

set(key: string, value: T): void;

clear(): void;

}

export function createStorage&lt;T&gt;(initialData?: T[]): Storage&lt;T&gt;;

// Exemplo de uso em TypeScript:

// const numberStorage = createStorage&lt;number&gt;();

// numberStorage.set(&#039;count&#039;, 42);</code></pre>

<p>Genéricos permitem que a mesma declaração funcione com múltiplos tipos, mantendo segurança em tempo de compilação.</p>

<h2>Estrutura de Projeto e Boas Práticas</h2>

<h3>Organização de Arquivos</h3>

<p>Em projetos maiores, organize múltiplos <code>.d.ts</code> em subdiretórios para refletir a estrutura da biblioteca:</p>

<pre><code>minha-biblioteca/

├── package.json

├── index.js

├── types/

│ ├── index.d.ts

│ ├── utils.d.ts

│ ├── services/

│ │ ├── user-service.d.ts

│ │ └── payment-service.d.ts

│ └── models/

│ ├── user.d.ts

│ └── payment.d.ts</code></pre>

<p>No <code>package.json</code>, indique o caminho para os tipos:</p>

<pre><code class="language-json">{

&quot;name&quot;: &quot;minha-biblioteca&quot;,

&quot;main&quot;: &quot;index.js&quot;,

&quot;types&quot;: &quot;types/index.d.ts&quot;

}</code></pre>

<p>O campo <code>&quot;types&quot;</code> diz ao TypeScript onde encontrar as declarações, melhorando a experiência do desenvolvedor que usa sua biblioteca.</p>

<h3>Reutilizando Declarações</h3>

<p>Quando uma declaração é usada em múltiplos arquivos <code>.d.ts</code>, importe-a:</p>

<pre><code class="language-typescript">// types/models/user.d.ts

export interface User {

id: number;

name: string;

email: string;

}

// types/services/user-service.d.ts

import { User } from &#039;../models/user&#039;;

export function getUser(id: number): Promise&lt;User&gt;;

export function updateUser(id: number, updates: Partial&lt;User&gt;): Promise&lt;User&gt;;</code></pre>

<p>Use <code>Partial&lt;User&gt;</code> para indicar que nem todas as propriedades precisam ser fornecidas na atualização. Isso torna o código mais flexível e mantém a reutilização.</p>

<h3>Documentação Inline</h3>

<p>JSDoc comentários em <code>.d.ts</code> aparecem no editor do desenvolvedor:</p>

<pre><code class="language-typescript">// calculator.d.ts

/**

  • Soma dois números.
  • @param a - O primeiro número
  • @param b - O segundo número
  • @returns A soma dos números
  • @example
  • const result = add(5, 3); // 8

*/

export function add(a: number, b: number): number;

/**

  • Opções de configuração para o calculador.
  • @property precision - Número de casas decimais (padrão: 2)
  • @property locale - Localização para formatação (padrão: &#039;en-US&#039;)

*/

export interface CalculatorOptions {

precision?: number;

locale?: string;

}</code></pre>

<p>Ao passar o mouse sobre <code>add</code> no VS Code, o desenvolvedor verá toda essa documentação. É um investimento pequeno que melhora muito a experiência.</p>

<h2>Exemplo Completo: Tipando uma Biblioteca Real</h2>

<p>Vamos tipar uma biblioteca fictícia mas realista chamada <code>form-validator</code>. O arquivo JavaScript original:</p>

<pre><code class="language-javascript">// form-validator.js

class FormValidator {

constructor(rules = {}) {

this.rules = rules;

this.errors = {};

}

addRule(fieldName, rule, message) {

if (!this.rules[fieldName]) {

this.rules[fieldName] = [];

}

this.rules[fieldName].push({ rule, message });

}

validate(data) {

this.errors = {};

for (const field in this.rules) {

const value = data[field];

for (const ruleObj of this.rules[field]) {

if (!ruleObj.rule(value)) {

if (!this.errors[field]) {

this.errors[field] = [];

}

this.errors[field].push(ruleObj.message);

}

}

}

return Object.keys(this.errors).length === 0;

}

getErrors() {

return this.errors;

}

}

module.exports = FormValidator;</code></pre>

<p>Agora o arquivo <code>.d.ts</code>:</p>

<pre><code class="language-typescript">// form-validator.d.ts

/**

  • Função que valida um valor.
  • @param value - O valor a ser validado
  • @returns true se válido, false caso contrário

*/

export type ValidatorRule = (value: any) =&gt; boolean;

/**

  • Definição de uma regra de validação com mensagem de erro.

*/

export interface ValidationRule {

rule: ValidatorRule;

message: string;

}

/**

  • Classe para validar dados de formulário com regras customizáveis.

*/

declare class FormValidator {

/**

  • Conjunto de regras organizadas por campo.

*/

rules: Record&lt;string, ValidationRule[]&gt;;

/**

  • Dicionário de erros encontrados na última validação.

*/

errors: Record&lt;string, string[]&gt;;

/**

  • Cria uma nova instância do validador.
  • @param rules - Objeto contendo regras pré-definidas (opcional)

*/

constructor(rules?: Record&lt;string, ValidationRule[]&gt;);

/**

  • Adiciona uma regra de validação a um campo.
  • @param fieldName - Nome do campo
  • @param rule - Função validadora
  • @param message - Mensagem de erro se a validação falhar

*/

addRule(fieldName: string, rule: ValidatorRule, message: string): void;

/**

  • Valida um objeto de dados contra as regras definidas.
  • @param data - Objeto contendo os dados a validar
  • @returns true se todos os dados são válidos, false caso contrário

*/

validate(data: Record&lt;string, any&gt;): boolean;

/**

  • Retorna os erros da última validação.
  • @returns Dicionário de erros por campo

*/

getErrors(): Record&lt;string, string[]&gt;;

}

export = FormValidator;</code></pre>

<p>Com este <code>.d.ts</code>, um desenvolvedor TypeScript pode usar a biblioteca com segurança de tipos total:</p>

<pre><code class="language-typescript">import FormValidator from &#039;form-validator&#039;;

const validator = new FormValidator();

validator.addRule(&#039;email&#039;, (value) =&gt; /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), &#039;Email inválido&#039;);

validator.addRule(&#039;age&#039;, (value) =&gt; value &gt;= 18, &#039;Deve ter 18 anos ou mais&#039;);

const isValid = validator.validate({ email: &#039;user@example.com&#039;, age: 25 });

if (!isValid) {

console.log(validator.getErrors());

}</code></pre>

<p>O TypeScript agora sabe exatamente quais métodos existem, quais parâmetros esperam e o que retornam.</p>

<h2>Conclusão</h2>

<p>Aprendemos que arquivos <code>.d.ts</code> são ferramentas essenciais para trazer segurança de tipos a bibliotecas JavaScript existentes. Primeiro, compreendemos que eles funcionam como um &quot;contrato&quot; de tipos que descreve interfaces públicas sem alterar o código JavaScript original. Segundo, dominamos a sintaxe fundamental: interfaces, tipos, genéricos e como declarar funções, classes e callbacks. Terceiro, internalizamos que boas práticas — como documentação inline, organização em diretórios e reutilização de tipos — transformam um simples <code>.d.ts</code> em uma experiência excelente para quem usa a biblioteca.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook: Declaration Files</a></li>

<li><a href="https://www.typescriptlang.org/docs/handbook/declaration-files/by-example.html" target="_blank" rel="noopener noreferrer">Microsoft: Writing Declaration Files</a></li>

<li><a href="https://github.com/DefinitelyTyped/DefinitelyTyped" target="_blank" rel="noopener noreferrer">DefinitelyTyped Repository</a> - Repositório com tipos para milhares de bibliotecas JavaScript</li>

<li><a href="https://www.typescriptlang.org/docs/handbook/utility-types.html" target="_blank" rel="noopener noreferrer">TypeScript: Utility Types</a> - Referência de tipos utilitários como Partial, Record e Required</li>

<li><a href="https://developer.mozilla.org/en-US/docs/Tools/JSDoc" target="_blank" rel="noopener noreferrer">Mozzila MDN: JSDoc</a> - Documentação sobre comentários JSDoc em arquivos de tipo</li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Guia Completo de Formulários com TypeScript: React Hook Form e Zod Integrados
Guia Completo de Formulários com TypeScript: React Hook Form e Zod Integrados

Introdução: Por que React Hook Form com Zod? Trabalhar com formulários em Rea...

Testes de Integração com TypeScript: Banco Real e Fixtures Tipadas: Do Básico ao Avançado
Testes de Integração com TypeScript: Banco Real e Fixtures Tipadas: Do Básico ao Avançado

O que são Testes de Integração e Por Que Importam Testes de integração valida...

Como Usar tRPC: APIs End-to-End Tipadas sem Geração de Código em Produção
Como Usar tRPC: APIs End-to-End Tipadas sem Geração de Código em Produção

O Problema das APIs Tradicionais Quando desenvolvemos aplicações modernas com...