TypeScript

Publicando Bibliotecas TypeScript: Types, Exports e Compatibilidade na Prática

14 min de leitura

Publicando Bibliotecas TypeScript: Types, Exports e Compatibilidade na Prática

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 de forma que a compilação gere não apenas JavaScript, mas também os arquivos (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 é 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

<h2>Preparando seu Projeto TypeScript para Publicação</h2>

<p>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.</p>

<p>A primeira etapa é configurar seu <code>tsconfig.json</code> de forma que a compilação gere não apenas JavaScript, mas também os arquivos <code>.d.ts</code> (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.</p>

<h2>Configuração de TypeScript e Geração de Type Definitions</h2>

<h3>Estruturando o tsconfig.json</h3>

<p>O arquivo <code>tsconfig.json</code> é 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.</p>

<pre><code class="language-json">{

&quot;compilerOptions&quot;: {

&quot;target&quot;: &quot;ES2020&quot;,

&quot;module&quot;: &quot;ESNext&quot;,

&quot;lib&quot;: [&quot;ES2020&quot;],

&quot;declaration&quot;: true,

&quot;declarationMap&quot;: true,

&quot;outDir&quot;: &quot;./dist&quot;,

&quot;rootDir&quot;: &quot;./src&quot;,

&quot;strict&quot;: true,

&quot;esModuleInterop&quot;: true,

&quot;skipLibCheck&quot;: true,

&quot;forceConsistentCasingInFileNames&quot;: true,

&quot;resolveJsonModule&quot;: true,

&quot;moduleResolution&quot;: &quot;node&quot;,

&quot;allowSyntheticDefaultImports&quot;: true

},

&quot;include&quot;: [&quot;src/*/&quot;],

&quot;exclude&quot;: [&quot;node_modules&quot;, &quot;dist&quot;, &quot;*/.test.ts&quot;]

}</code></pre>

<p>A opção <code>declaration: true</code> instrui o TypeScript a gerar um arquivo <code>.d.ts</code> para cada arquivo TypeScript compilado. <code>declarationMap: true</code> 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 <code>target: &quot;ES2020&quot;</code> garante compatibilidade com navegadores modernos enquanto mantém características recentes de JavaScript. Note que <code>module: &quot;ESNext&quot;</code> permite que ferramentas de empacotamento como webpack e rollup façam tree-shaking, reduzindo o tamanho final do bundle dos consumidores.</p>

<h3>Exemplo Prático de Type Definitions</h3>

<p>Considere uma biblioteca simples de utilitários matemáticos. Seu arquivo fonte seria:</p>

<pre><code class="language-typescript">// 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;

}

}</code></pre>

<p>Após executar <code>tsc</code>, você terá em <code>dist/math.d.ts</code>:</p>

<pre><code class="language-typescript">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;

}</code></pre>

<p>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.</p>

<h2>Configuração do package.json e Estratégias de Export</h2>

<h3>Definindo Entry Points</h3>

<p>O <code>package.json</code> é 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.).</p>

<pre><code class="language-json">{

&quot;name&quot;: &quot;@seu-nome/math-utils&quot;,

&quot;version&quot;: &quot;1.0.0&quot;,

&quot;description&quot;: &quot;Biblioteca de utilitários matemáticos com suporte total a TypeScript&quot;,

&quot;main&quot;: &quot;./dist/index.js&quot;,

&quot;types&quot;: &quot;./dist/index.d.ts&quot;,

&quot;exports&quot;: {

&quot;.&quot;: {

&quot;import&quot;: &quot;./dist/index.mjs&quot;,

&quot;require&quot;: &quot;./dist/index.js&quot;,

&quot;types&quot;: &quot;./dist/index.d.ts&quot;

},

&quot;./math&quot;: {

&quot;import&quot;: &quot;./dist/math.mjs&quot;,

&quot;require&quot;: &quot;./dist/math.js&quot;,

&quot;types&quot;: &quot;./dist/math.d.ts&quot;

}

},

&quot;files&quot;: [

&quot;dist&quot;,

&quot;README.md&quot;,

&quot;LICENSE&quot;

],

&quot;keywords&quot;: [&quot;math&quot;, &quot;typescript&quot;, &quot;utils&quot;],

&quot;author&quot;: &quot;Seu Nome&quot;,

&quot;license&quot;: &quot;MIT&quot;,

&quot;repository&quot;: {

&quot;type&quot;: &quot;git&quot;,

&quot;url&quot;: &quot;https://github.com/seu-nome/math-utils&quot;

}

}</code></pre>

<p>O campo <code>exports</code> é 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 <code>types</code> aponta para onde encontrar as declarações de tipo do ponto de entrada principal.</p>

<h3>Estrutura de Exportação Modular</h3>

<p>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:</p>

<pre><code>src/

├── index.ts

├── math.ts

├── string.ts

└── array.ts</code></pre>

<p>Com o <code>exports</code> apropriado no <code>package.json</code>, consumidores podem fazer:</p>

<pre><code class="language-typescript">// Importação do ponto de entrada principal

import { add, multiply } from &#039;@seu-nome/math-utils&#039;;

// Ou importações granulares

import { add } from &#039;@seu-nome/math-utils/math&#039;;

import { capitalize } from &#039;@seu-nome/math-utils/string&#039;;</code></pre>

<p>Para isso funcionar, cada arquivo em <code>src/</code> deve ter seu próprio ponto de entrada em <code>dist/</code>, e o <code>package.json</code> deve listar todos eles na seção <code>exports</code>. Seu script de build (usando tsc ou outra ferramenta) deve gerar tanto <code>.js</code> quanto <code>.mjs</code> para máxima compatibilidade.</p>

<h2>Compatibilidade e Versionamento Semântico</h2>

<h3>Entendendo Mudanças Quebradoras vs Compatíveis</h3>

<p>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 &quot;quebradora&quot; (breaking change) se força consumidores a atualizar seu código para continuar usando sua biblioteca.</p>

<p>Exemplos de mudanças <strong>compatíveis</strong>:</p>

<ul>

<li>Adicionar um novo parâmetro opcional a uma função</li>

<li>Adicionar uma propriedade opcional a uma interface</li>

</ul>

<p>- Alargar o tipo de retorno (ex: <code>number</code> → <code>number | string</code>)</p>

<ul>

<li>Adicionar novos exports sem remover os antigos</li>

</ul>

<p>Exemplos de mudanças <strong>quebradoras</strong>:</p>

<ul>

<li>Remover ou renomear uma função ou classe exportada</li>

<li>Tornar um parâmetro obrigatório quando era opcional</li>

<li>Estreitar o tipo de um parâmetro</li>

<li>Remover uma propriedade de uma interface exportada</li>

</ul>

<h3>Versionamento Semântico na Prática</h3>

<pre><code class="language-json">{

&quot;version&quot;: &quot;2.3.1&quot;

}</code></pre>

<p>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.</p>

<pre><code class="language-typescript">// 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 || &#039;&#039;);

}

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

}</code></pre>

<p>Ao publicar no npm, você comunica essa história através das tags de versão no seu repositório. Consumidores que especificam <code>&quot;@seu-nome/math-utils&quot;: &quot;^1.1.0&quot;</code> receberão atualizações até (mas não incluindo) versão 2.0.0 automaticamente.</p>

<h2>Construindo e Testando sua Biblioteca para Publicação</h2>

<h3>Script de Build Profissional</h3>

<p>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 <code>tsc</code> com scripts auxiliares:</p>

<pre><code class="language-json">{

&quot;scripts&quot;: {

&quot;build&quot;: &quot;npm run clean &amp;&amp; npm run compile &amp;&amp; npm run validate-types&quot;,

&quot;clean&quot;: &quot;rm -rf dist&quot;,

&quot;compile&quot;: &quot;tsc&quot;,

&quot;validate-types&quot;: &quot;tsc --noEmit&quot;,

&quot;test&quot;: &quot;jest&quot;,

&quot;prepublishOnly&quot;: &quot;npm run test &amp;&amp; npm run build&quot;,

&quot;lint&quot;: &quot;eslint src --ext .ts&quot;

}

}</code></pre>

<p>O script <code>prepublishOnly</code> é 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 <code>validate-types</code> roda TypeScript sem emitir arquivos (apenas fazendo type-checking), o que é rápido e garante que não há erros de tipo.</p>

<h3>Testando Compatibilidade com Consumidores</h3>

<p>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:</p>

<pre><code class="language-typescript">// tests/integration.test.ts

import { add, multiply, ChainedCalculator } from &#039;../src/math&#039;;

describe(&#039;Math Utils Integration&#039;, () =&gt; {

test(&#039;add function works correctly&#039;, () =&gt; {

expect(add(2, 3)).toBe(5);

});

test(&#039;ChainedCalculator allows method chaining&#039;, () =&gt; {

const result = new ChainedCalculator(5)

.add(3)

.multiply(2)

.value;

expect(result).toBe(16);

});

test(&#039;types are correctly exposed&#039;, () =&gt; {

const calc: ChainedCalculator = new ChainedCalculator();

expect(calc).toBeDefined();

});

});</code></pre>

<p>Além dos testes unitários, você pode criar um projeto de teste separado que importa sua biblioteca localmente (usando <code>npm link</code> ou <code>npm pack</code>) e verifica se os tipos funcionam corretamente em um ambiente real:</p>

<pre><code class="language-typescript">// test-consumer/index.ts

import { add, ChainedCalculator } from &#039;math-utils&#039;;

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

<h2>Conclusão</h2>

<p>Publicar uma biblioteca TypeScript profissional envolve três pilares principais. Primeiro, <strong>configurar corretamente a geração de type definitions</strong> através do <code>tsconfig.json</code>, garantindo que consumidores recebam informações de tipo precisas e completas — isso diferencia uma biblioteca TypeScript competente de uma genérica. Segundo, <strong>dominar o <code>package.json</code> e o campo <code>exports</code></strong>, permitindo que diferentes tipos de consumidores (CommonJS, ES modules, TypeScript puro, JavaScript puro) usem sua biblioteca sem fricção. Terceiro, <strong>respeitar o versionamento semântico e testar compatibilidade</strong>, pois a reputação de sua biblioteca depende de confiabilidade — quebras inesperadas alienam usuários rapidamente.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook: Declaration Files</a></li>

<li><a href="https://docs.npmjs.com/cli/v9/configuring-npm/package-json" target="_blank" rel="noopener noreferrer">npm package.json documentation</a></li>

<li><a href="https://nodejs.org/api/packages.html#packages_exports" target="_blank" rel="noopener noreferrer">Node.js Package Exports documentation</a></li>

<li><a href="https://semver.org/" target="_blank" rel="noopener noreferrer">Semantic Versioning 2.0.0</a></li>

<li><a href="https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html" target="_blank" rel="noopener noreferrer">TypeScript Library Development Best Practices</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Como Usar tRPC: APIs End-to-End Tipadas sem Geração de Código em Produção
Como Usar tRPC: APIs End-to-End Tipadas sem Geração de Código em Produção

O Problema das APIs Tradicionais Quando desenvolvemos aplicações modernas com...

Funções em TypeScript: Assinaturas, Overloads e this Tipado na Prática
Funções em TypeScript: Assinaturas, Overloads e this Tipado na Prática

Fundamentos de Funções em TypeScript Uma função em TypeScript é bem mais que...

Boas Práticas de React Query com TypeScript: Queries, Mutations e Tipos Inferidos para Times Ágeis
Boas Práticas de React Query com TypeScript: Queries, Mutations e Tipos Inferidos para Times Ágeis

Por que React Query Revolucionou o Gerenciamento de Estado Durante minha carr...