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

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

O que são Arquivos .d.ts e Por Que Importam

Os arquivos .d.ts (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 .d.ts 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 any, perdendo todos os benefícios de segurança de tipos. O arquivo .d.ts 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 .d.ts usa a mesma sintaxe que arquivos .ts 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 math-helper.js:

// math-helper.js (JavaScript puro)
function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

module.exports = { add, multiply };

O arquivo .d.ts correspondente seria:

// math-helper.d.ts
export function add(a: number, b: number): number;
export function multiply(a: number, b: number): number;

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

Declarando Interfaces e Tipos

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

// 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<User>;
export function createUser(data: CreateUserRequest): Promise<User>;
export function deleteUser(id: number): Promise<void>;

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

Tipando Padrões Comuns de Bibliotecas JavaScript

Classes e Construtores

Muitas bibliotecas JavaScript expõem classes. No .d.ts, declare usando declare class:

// event-emitter.d.ts
declare class EventEmitter {
  constructor();
  on(eventName: string, callback: (data: any) => void): void;
  emit(eventName: string, data?: any): void;
  off(eventName: string, callback: (data: any) => void): void;
}

export = EventEmitter;

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

Callbacks e Funções de Ordem Superior

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

// request-helper.d.ts
export type RequestCallback = (error: Error | null, data?: any) => void;

export function fetchData(
  url: string,
  options?: RequestOptions,
  callback?: RequestCallback
): void;

export interface RequestOptions {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers?: Record<string, string>;
  timeout?: number;
}

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

Genéricos

Se a biblioteca usa tipos genéricos (como contêineres que aceitam qualquer tipo), descreva usando <T>:

// storage.d.ts
export interface Storage<T> {
  get(key: string): T | undefined;
  set(key: string, value: T): void;
  clear(): void;
}

export function createStorage<T>(initialData?: T[]): Storage<T>;

// Exemplo de uso em TypeScript:
// const numberStorage = createStorage<number>();
// numberStorage.set('count', 42);

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

Estrutura de Projeto e Boas Práticas

Organização de Arquivos

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

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

No package.json, indique o caminho para os tipos:

{
  "name": "minha-biblioteca",
  "main": "index.js",
  "types": "types/index.d.ts"
}

O campo "types" diz ao TypeScript onde encontrar as declarações, melhorando a experiência do desenvolvedor que usa sua biblioteca.

Reutilizando Declarações

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

// types/models/user.d.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

// types/services/user-service.d.ts
import { User } from '../models/user';

export function getUser(id: number): Promise<User>;
export function updateUser(id: number, updates: Partial<User>): Promise<User>;

Use Partial<User> 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.

Documentação Inline

JSDoc comentários em .d.ts aparecem no editor do desenvolvedor:

// 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: 'en-US')
 */
export interface CalculatorOptions {
  precision?: number;
  locale?: string;
}

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

Exemplo Completo: Tipando uma Biblioteca Real

Vamos tipar uma biblioteca fictícia mas realista chamada form-validator. O arquivo JavaScript original:

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

Agora o arquivo .d.ts:

// 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) => 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<string, ValidationRule[]>;

  /**
   * Dicionário de erros encontrados na última validação.
   */
  errors: Record<string, string[]>;

  /**
   * Cria uma nova instância do validador.
   * @param rules - Objeto contendo regras pré-definidas (opcional)
   */
  constructor(rules?: Record<string, ValidationRule[]>);

  /**
   * 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<string, any>): boolean;

  /**
   * Retorna os erros da última validação.
   * @returns Dicionário de erros por campo
   */
  getErrors(): Record<string, string[]>;
}

export = FormValidator;

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

import FormValidator from 'form-validator';

const validator = new FormValidator();

validator.addRule('email', (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), 'Email inválido');
validator.addRule('age', (value) => value >= 18, 'Deve ter 18 anos ou mais');

const isValid = validator.validate({ email: 'user@example.com', age: 25 });
if (!isValid) {
  console.log(validator.getErrors());
}

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

Conclusão

Aprendemos que arquivos .d.ts são ferramentas essenciais para trazer segurança de tipos a bibliotecas JavaScript existentes. Primeiro, compreendemos que eles funcionam como um "contrato" 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 .d.ts em uma experiência excelente para quem usa a biblioteca.

Referências


Artigos relacionados