Guia Completo de TypeScript para Bibliotecas: Escrevendo .d.ts e Tipos Públicos Já leu

Por que TypeScript é Essencial para Bibliotecas Quando você cria uma biblioteca JavaScript, seus usuários não têm visibilidade do código interno — apenas da interface pública. TypeScript resolve esse problema fornecendo um contrato explícito através de tipos. Arquivos (declaration files) são a ponte entre sua implementação e quem usa sua biblioteca. Sem eles, desenvolvedores enfrentam falta de autocompletar, erros silenciosos em tempo de desenvolvimento e documentação incompleta. A diferença é imediata: uma biblioteca sem tipos força o usuário a ler documentação externa ou fazer tentativas; uma com tipos bem definidos oferece autocompletar inteligente, detecção de erros antes da execução e melhor experiência geral. Vamos aprender como criar tipos públicos que sua comunidade agradecerá. Estruturando Tipos Públicos com .d.ts Criando Declaration Files O arquivo contém apenas tipos, interfaces e assinaturas de funções — nenhuma implementação. Se você já tem código TypeScript compilado, o compilador pode gerar automaticamente esses arquivos com no . Mas entender como escrever manualmente é crucial para controlar

Por que TypeScript é Essencial para Bibliotecas

Quando você cria uma biblioteca JavaScript, seus usuários não têm visibilidade do código interno — apenas da interface pública. TypeScript resolve esse problema fornecendo um contrato explícito através de tipos. Arquivos .d.ts (declaration files) são a ponte entre sua implementação e quem usa sua biblioteca. Sem eles, desenvolvedores enfrentam falta de autocompletar, erros silenciosos em tempo de desenvolvimento e documentação incompleta.

A diferença é imediata: uma biblioteca sem tipos força o usuário a ler documentação externa ou fazer tentativas; uma com tipos bem definidos oferece autocompletar inteligente, detecção de erros antes da execução e melhor experiência geral. Vamos aprender como criar tipos públicos que sua comunidade agradecerá.

Estruturando Tipos Públicos com .d.ts

Criando Declaration Files

O arquivo .d.ts contém apenas tipos, interfaces e assinaturas de funções — nenhuma implementação. Se você já tem código TypeScript compilado, o compilador pode gerar automaticamente esses arquivos com declaration: true no tsconfig.json. Mas entender como escrever manualmente é crucial para controlar exatamente o que exponha.

// src/types/index.d.ts
export interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

export interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

export declare class UserService {
  constructor(apiUrl: string);
  getUser(id: number): Promise<ApiResponse<User>>;
  createUser(user: Omit<User, 'id' | 'createdAt'>): Promise<User>;
  deleteUser(id: number): Promise<void>;
}

export declare function validateEmail(email: string): boolean;

Aqui definimos tipos (User, ApiResponse), uma classe (UserService) e uma função (validateEmail). Tudo é declarado publicamente. A correspondência com a implementação real deve ser exata — TypeScript verificará isso na compilação.

Controlando Visibilidade com Modificadores

Nem tudo precisa ser público. Use private, protected e membros não exportados para esconder detalhes internos. Sua biblioteca fica mais limpa e o contrato mais claro.

// src/userService.ts
class UserService {
  private apiUrl: string;
  private cache: Map<number, User> = new Map();

  constructor(apiUrl: string) {
    this.apiUrl = apiUrl;
  }

  private validateApiConnection(): Promise<boolean> {
    // lógica interna
    return Promise.resolve(true);
  }

  public async getUser(id: number): Promise<ApiResponse<User>> {
    if (this.cache.has(id)) {
      return {
        data: this.cache.get(id)!,
        status: 200,
        message: 'From cache'
      };
    }
    // chamada real
    const response = await fetch(`${this.apiUrl}/users/${id}`);
    const data = await response.json();
    this.cache.set(id, data);
    return { data, status: 200, message: 'Success' };
  }
}

Propriedades private como apiUrl e cache não aparecem no .d.ts gerado automaticamente. Apenas getUser é exposto. Isso é exatamente o que você quer — apenas a interface pública.

Configuração do tsconfig.json para Geração Automática

Habilitando Declaration Emission

Para gerar .d.ts automaticamente durante a compilação, configure seu tsconfig.json:

{
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "./dist/types",
    "emitDeclarationOnly": false,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "module": "commonjs",
    "target": "ES2020"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

A opção declaration: true instrui o TypeScript a gerar um arquivo .d.ts para cada arquivo .ts. Use declarationDir para organizá-los separadamente. No package.json, aponte para esses tipos:

{
  "name": "minha-biblioteca",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/types/index.d.ts",
  "scripts": {
    "build": "tsc"
  }
}

O campo "types" é a chave — ele diz ao TypeScript e IDEs exatamente onde encontrar seus tipos.

Padrões Avançados para Tipos Robustos

Tipos Genéricos e Utilitários

Para bibliotecas reutilizáveis, genéricos e tipos utilitários são indispensáveis. Permitem flexibilidade mantendo segurança de tipo.

// src/database.d.ts
export interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  create(item: Omit<T, 'id'>): Promise<T>;
  update(id: string, item: Partial<T>): Promise<T>;
  delete(id: string): Promise<boolean>;
}

export interface Entity {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

export interface Product extends Entity {
  name: string;
  price: number;
  stock: number;
}

export declare class Database {
  getRepository<T extends Entity>(
    collection: string
  ): Repository<T>;
}

// Uso:
// const db = new Database();
// const products = db.getRepository<Product>('products');
// products.findById('123'); // Promise<Product | null> ✓

Aqui Repository<T> é genérico — adapta-se a qualquer tipo que estenda Entity. Usuários ganham type-safety específico para seu domínio sem duplicar código.

Overloads para Flexibilidade

Funções com comportamentos diferentes conforme argumentos precisam de overloads:

export interface SearchOptions {
  limit?: number;
  offset?: number;
}

// Sobrecargas
export declare function search(
  query: string
): Promise<User[]>;

export declare function search(
  query: string,
  options: SearchOptions
): Promise<User[]>;

export declare function search(
  query: string,
  options?: SearchOptions
): Promise<User[]>;

TypeScript escolhe automaticamente o overload correto baseado nos argumentos passados. Sem overloads, a IDE não saberia se options é obrigatório ou não.

Conclusão

Aprender a escrever tipos públicos com .d.ts é o diferencial entre uma biblioteca amadora e uma profissional. Três pontos essenciais ficaram claros: (1) tipos explícitos transformam a experiência do usuário através de autocompletar e detecção de erros; (2) controle de visibilidade (público vs. privado) mantém sua API limpa e documentada naturalmente; (3) padrões como genéricos e overloads permitem flexibilidade sem sacrificar segurança de tipo. Configure seu tsconfig.json corretamente e deixe o compilador fazer o trabalho pesado de gerar tipos automaticamente. Sua comunidade agradecerá.

Referências


Artigos relacionados