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
- TypeScript Handbook: Declaration Files
- Microsoft: Writing Declaration Files
- DefinitelyTyped Repository - Repositório com tipos para milhares de bibliotecas JavaScript
- TypeScript: Utility Types - Referência de tipos utilitários como Partial, Record e Required
- Mozzila MDN: JSDoc - Documentação sobre comentários JSDoc em arquivos de tipo