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á.