Type-only imports e exports: otimizando bundles

1. O Problema: Tipos que Inflam o Bundle Final

1.1. Como imports comuns de tipos poluem o JavaScript gerado

Quando você utiliza import tradicional para trazer tipos, o TypeScript pode gerar código JavaScript desnecessário. Considere o seguinte exemplo:

// utils.ts
export interface Usuario {
  nome: string;
  idade: number;
}

export function formatarNome(usuario: Usuario): string {
  return `${usuario.nome} - ${usuario.idade} anos`;
}

// app.ts
import { Usuario, formatarNome } from './utils';

No JavaScript gerado, a interface Usuario desaparece (pois é apenas tipo), mas o import original pode gerar referências residuais que confundem bundlers.

1.2. Diferença entre valor (runtime) e tipo (compile-time)

TypeScript opera em dois níveis: tipos (que existem apenas em tempo de compilação) e valores (que existem em runtime). Um interface, type ou type alias é puramente tipo. Já funções, classes e objetos são valores.

// tipo puro - desaparece no JS
type Status = 'ativo' | 'inativo';

// valor - permanece no JS
const STATUS_MAP = { ativo: 1, inativo: 0 };

1.3. Cenário típico: importar uma interface e um utilitário do mesmo módulo

// modulo.ts
export interface Config {
  debug: boolean;
}
export function init(config: Config) { /* ... */ }

// consumidor.ts
import { Config, init } from './modulo'; // Config é tipo, init é valor

Sem import type, o bundler pode manter todo o módulo no bundle, mesmo que apenas o tipo seja necessário.

2. import type e export type: A Sintaxe Essencial

2.1. Declaração explícita

// ✅ Correto: apenas tipos são importados
import type { Config, Status } from './types';

// ❌ Erro: não é possível usar Config em runtime
const config: Config = { debug: true }; // Isso funciona (tipo)
console.log(Config); // Erro: Config é apenas um tipo

2.2. Exportando apenas tipos

// types.ts
export type MeuType = string | number;
export type MinhaInterface = { id: string };

// re-exportando apenas tipos
export type { MeuType, MinhaInterface };

2.3. Combinando imports de valor e tipo na mesma linha

import { funcao, type Tipo } from './modulo';

// funcao é valor (disponível em runtime)
// Tipo é apenas tipo (removido no JS)

3. type Modifier em Re-exports e Namespaces

3.1. Re-exportando tipos

// index.ts - barrel file
export type { Config } from './config';
export { init } from './config';
export type { Usuario } from './usuario';

3.2. Uso do modificador type em import dinâmico

// Import dinâmico com tipo
const { type MeuTipo } = await import('./types');

3.3. Limpeza de exports em barrel files

// ❌ Antes: polui o bundle
export { Usuario, formatarNome } from './usuario';

// ✅ Depois: separa tipos de valores
export type { Usuario } from './usuario';
export { formatarNome } from './usuario';

4. isolatedModules e a Obrigatoriedade de Type-only Imports

4.1. O que é isolatedModules

Quando ativado no tsconfig.json, cada arquivo é transpilado de forma independente. Isso exige que o TypeScript saiba exatamente o que é tipo e o que é valor.

{
  "compilerOptions": {
    "isolatedModules": true
  }
}

4.2. Erro clássico

// ❌ Gera erro com isolatedModules
export { MinhaInterface } from './types';

// ✅ Correção
export type { MinhaInterface } from './types';

4.3. Configuração verbatimModuleSyntax

Uma alternativa moderna que força a sintaxe explícita:

{
  "compilerOptions": {
    "verbatimModuleSyntax": true
  }
}
// ✅ Obrigatório com verbatimModuleSyntax
import type { MeuTipo } from './types';
import { funcao } from './utils';

5. Impacto Real em Bundlers (Webpack, Vite, esbuild, SWC)

5.1. Como cada bundler lida com type-only imports

Todos os bundlers modernos (Webpack 5+, Vite, esbuild, SWC) realizam tree-shaking eficiente quando encontram import type. Eles removem completamente as referências a tipos do bundle final.

5.2. Diferença prática: bundle com e sem import type

// Sem type-only import
import { Usuario, formatarNome } from './usuario';
// Bundle: ~2KB (inclui referências desnecessárias)

// Com type-only import
import type { Usuario } from './usuario';
import { formatarNome } from './usuario';
// Bundle: ~1.2KB (apenas o necessário)

5.3. Casos extremos

Em bibliotecas com dezenas de tipos exportados (como @types/react), o uso correto de import type pode reduzir o bundle em até 40%:

// ❌ Importa tudo
import { ReactNode, useState } from 'react';

// ✅ Importa apenas o necessário
import type { ReactNode } from 'react';
import { useState } from 'react';

6. Boas Práticas em Projetos Grandes e Monorepos

6.1. Regras do ESLint

Configure @typescript-eslint/consistent-type-imports para enforce automático:

{
  "rules": {
    "@typescript-eslint/consistent-type-imports": [
      "error",
      { "prefer": "type-imports" }
    ]
  }
}

6.2. Autofix e migração de código legado

# Comando para aplicar autofix em todo o projeto
npx eslint . --fix --rule '@typescript-eslint/consistent-type-imports: error'

6.3. Organização de tipos compartilhados

// packages/types/src/index.ts
export type { Usuario } from './usuario';
export type { Config } from './config';

// packages/utils/src/index.ts
export { formatarNome } from './formatacao';
export type { Formato } from './formatacao';

7. Armadilhas e Casos Especiais

7.1. Classes: quando import type não funciona

Classes são valores em runtime, mesmo que também possam ser usadas como tipos:

// ❌ Não funciona: classe precisa existir em runtime
import type { MinhaClasse } from './classes';

// ✅ Correto: importa como valor
import { MinhaClasse } from './classes';

7.2. Enums const vs. enums tradicionais

// ✅ const enum - pode ser importado como tipo (inline)
import type { Status } from './enums';

// ❌ enum tradicional - precisa de runtime
import { Status } from './enums'; // Erro se for type-only

7.3. Decorators e metadados

Decorators exigem runtime, mesmo para tipos:

// ❌ Decorator precisa de runtime
import type { Decorator } from './decorators';

// ✅ Correto
import { Decorator } from './decorators';

Conclusão

O uso correto de import type e export type é essencial para otimizar bundles em projetos TypeScript modernos. A prática reduz o tamanho final do JavaScript gerado, melhora o tree-shaking dos bundlers e torna o código mais explícito quanto à intenção de uso. Com ferramentas como ESLint, isolatedModules e verbatimModuleSyntax, é possível automatizar essa otimização e evitar erros comuns.

Lembre-se: tipos são para o compilador, valores são para o runtime. Mantenha essa distinção clara e seu bundle agradecerá.

Referências