Preparando seu Projeto TypeScript para Publicação
Antes de publicar uma biblioteca TypeScript, você precisa entender que um projeto pronto para distribuição não é simplesmente um repositório com código TypeScript compilado. É necessário configurar corretamente o ambiente, definir metadados apropriados e garantir que os consumidores da sua biblioteca recebam tanto o código compilado quanto as informações de tipo (type definitions) sem ambiguidades.
A primeira etapa é configurar seu tsconfig.json de forma que a compilação gere não apenas JavaScript, mas também os arquivos .d.ts (declaration files). Esses arquivos contêm as definições de tipo e são críticos para desenvolvedores que usam sua biblioteca em projetos TypeScript. Além disso, você precisa decidir qual será a estrutura de diretórios no seu pacote distribuído e como os consumidores importarão seu código.
Configuração de TypeScript e Geração de Type Definitions
Estruturando o tsconfig.json
O arquivo tsconfig.json é o coração da configuração de qualquer projeto TypeScript. Para uma biblioteca que será publicada, você deve focar em três aspectos: a geração automática de declarações de tipo, a escolha do alvo de compilação (ECMAScript version) e a definição clara dos diretórios de entrada e saída.
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
},
"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 TypeScript compilado. declarationMap: true cria source maps para essas declarações, permitindo que desenvolvedores naveguem até o código-fonte original ao usar sua biblioteca em um IDE. O target: "ES2020" garante compatibilidade com navegadores modernos enquanto mantém características recentes de JavaScript. Note que module: "ESNext" permite que ferramentas de empacotamento como webpack e rollup façam tree-shaking, reduzindo o tamanho final do bundle dos consumidores.
Exemplo Prático de Type Definitions
Considere uma biblioteca simples de utilitários matemáticos. Seu arquivo fonte seria:
// src/math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export interface Calculator {
value: number;
add(n: number): Calculator;
multiply(n: number): Calculator;
}
export class ChainedCalculator implements Calculator {
value: number = 0;
constructor(initialValue: number = 0) {
this.value = initialValue;
}
add(n: number): Calculator {
this.value += n;
return this;
}
multiply(n: number): Calculator {
this.value *= n;
return this;
}
}
Após executar tsc, você terá em dist/math.d.ts:
export declare function add(a: number, b: number): number;
export declare function multiply(a: number, b: number): number;
export interface Calculator {
value: number;
add(n: number): Calculator;
multiply(n: number): Calculator;
}
export declare class ChainedCalculator implements Calculator {
value: number;
constructor(initialValue?: number);
add(n: number): Calculator;
multiply(n: number): Calculator;
}
Essa declaração é automaticamente gerada e garante que consumidores da sua biblioteca terão acesso completo a informações de tipo, mesmo se estiverem usando JavaScript puro ou TypeScript.
Configuração do package.json e Estratégias de Export
Definindo Entry Points
O package.json é o documento que descreve sua biblioteca para o npm. Para uma biblioteca TypeScript publicada, você precisa indicar qual arquivo é o ponto de entrada principal, onde estão as type definitions e, se aplicável, oferecer suporte a diferentes módulos (CommonJS, ES Modules, etc.).
{
"name": "@seu-nome/math-utils",
"version": "1.0.0",
"description": "Biblioteca de utilitários matemáticos com suporte total a TypeScript",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./math": {
"import": "./dist/math.mjs",
"require": "./dist/math.js",
"types": "./dist/math.d.ts"
}
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"keywords": ["math", "typescript", "utils"],
"author": "Seu Nome",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/seu-nome/math-utils"
}
}
O campo exports é uma das inovações mais importantes na eclosfera do Node.js. Ele permite que você defina múltiplos pontos de entrada e especifique qual arquivo usar para diferentes contextos (importações ES modules vs CommonJS). Isso é crucial para compatibilidade máxima. O campo types aponta para onde encontrar as declarações de tipo do ponto de entrada principal.
Estrutura de Exportação Modular
Se sua biblioteca é grande, você pode oferecer importações granulares. Isso reduz o tamanho dos bundles dos consumidores. Suponha que sua estrutura de diretórios seja:
src/
├── index.ts
├── math.ts
├── string.ts
└── array.ts
Com o exports apropriado no package.json, consumidores podem fazer:
// Importação do ponto de entrada principal
import { add, multiply } from '@seu-nome/math-utils';
// Ou importações granulares
import { add } from '@seu-nome/math-utils/math';
import { capitalize } from '@seu-nome/math-utils/string';
Para isso funcionar, cada arquivo em src/ deve ter seu próprio ponto de entrada em dist/, e o package.json deve listar todos eles na seção exports. Seu script de build (usando tsc ou outra ferramenta) deve gerar tanto .js quanto .mjs para máxima compatibilidade.
Compatibilidade e Versionamento Semântico
Entendendo Mudanças Quebradoras vs Compatíveis
A compatibilidade de uma biblioteca TypeScript vai além de apenas manter a mesma interface de função. Inclui a compatibilidade estrutural de tipos, que é o sistema de tipos do TypeScript. Uma mudança é considerada "quebradora" (breaking change) se força consumidores a atualizar seu código para continuar usando sua biblioteca.
Exemplos de mudanças compatíveis:
- Adicionar um novo parâmetro opcional a uma função
- Adicionar uma propriedade opcional a uma interface
- Alargar o tipo de retorno (ex: number → number | string)
- Adicionar novos exports sem remover os antigos
Exemplos de mudanças quebradoras:
- Remover ou renomear uma função ou classe exportada
- Tornar um parâmetro obrigatório quando era opcional
- Estreitar o tipo de um parâmetro
- Remover uma propriedade de uma interface exportada
Versionamento Semântico na Prática
{
"version": "2.3.1"
}
Essa string segue o padrão MAJOR.MINOR.PATCH. MAJOR é incrementado para mudanças quebradoras, MINOR para novas features compatíveis com versões anteriores, e PATCH para correções de bugs. Se sua versão é 2.3.1 e você introduz uma mudança quebradora, a próxima versão deve ser 3.0.0. Se apenas adiciona uma função nova, é 2.4.0. Se corrige um bug, é 2.3.2.
// Versão 1.0.0 - Sua biblioteca original
export function process(data: string): string {
return data.toUpperCase();
}
// Versão 1.1.0 - Adiciona parâmetro opcional (compatível)
export function process(data: string, separator?: string): string {
return data.toUpperCase() + (separator || '');
}
// Versão 2.0.0 - Remove a função antiga e cria uma nova (quebradora)
export function transform(data: string, options: { uppercase?: boolean } = {}): string {
return options.uppercase !== false ? data.toUpperCase() : data;
}
Ao publicar no npm, você comunica essa história através das tags de versão no seu repositório. Consumidores que especificam "@seu-nome/math-utils": "^1.1.0" receberão atualizações até (mas não incluindo) versão 2.0.0 automaticamente.
Construindo e Testando sua Biblioteca para Publicação
Script de Build Profissional
Um script de build deve não apenas compilar TypeScript para JavaScript, mas também gerar as variantes necessárias (CommonJS e ES Modules) e preparar os arquivos de declaração. Uma abordagem moderna usa tsc com scripts auxiliares:
{
"scripts": {
"build": "npm run clean && npm run compile && npm run validate-types",
"clean": "rm -rf dist",
"compile": "tsc",
"validate-types": "tsc --noEmit",
"test": "jest",
"prepublishOnly": "npm run test && npm run build",
"lint": "eslint src --ext .ts"
}
}
O script prepublishOnly é especial: ele é executado automaticamente pelo npm antes de publicar seu pacote, garantindo que você nunca publique uma versão sem testes passando e sem compilação bem-sucedida. O validate-types roda TypeScript sem emitir arquivos (apenas fazendo type-checking), o que é rápido e garante que não há erros de tipo.
Testando Compatibilidade com Consumidores
Antes de publicar, você deve testar como seus consumidores experimentarão a biblioteca. Uma prática excelente é testar tanto em projetos TypeScript quanto em projetos JavaScript:
// tests/integration.test.ts
import { add, multiply, ChainedCalculator } from '../src/math';
describe('Math Utils Integration', () => {
test('add function works correctly', () => {
expect(add(2, 3)).toBe(5);
});
test('ChainedCalculator allows method chaining', () => {
const result = new ChainedCalculator(5)
.add(3)
.multiply(2)
.value;
expect(result).toBe(16);
});
test('types are correctly exposed', () => {
const calc: ChainedCalculator = new ChainedCalculator();
expect(calc).toBeDefined();
});
});
Além dos testes unitários, você pode criar um projeto de teste separado que importa sua biblioteca localmente (usando npm link ou npm pack) e verifica se os tipos funcionam corretamente em um ambiente real:
// test-consumer/index.ts
import { add, ChainedCalculator } from 'math-utils';
// Isso deve compilar sem erros se os tipos estão corretos
const result: number = add(1, 2);
const calc = new ChainedCalculator();
// calc é do tipo Calculator aqui, TypeScript sabe disso
const chainedResult: number = calc.add(5).multiply(2).value;
Conclusão
Publicar uma biblioteca TypeScript profissional envolve três pilares principais. Primeiro, configurar corretamente a geração de type definitions através do tsconfig.json, garantindo que consumidores recebam informações de tipo precisas e completas — isso diferencia uma biblioteca TypeScript competente de uma genérica. Segundo, dominar o package.json e o campo exports, permitindo que diferentes tipos de consumidores (CommonJS, ES modules, TypeScript puro, JavaScript puro) usem sua biblioteca sem fricção. Terceiro, respeitar o versionamento semântico e testar compatibilidade, pois a reputação de sua biblioteca depende de confiabilidade — quebras inesperadas alienam usuários rapidamente.