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
- TypeScript Handbook: Type-Only Imports and Exports — Documentação oficial sobre a sintaxe
import typeeexport typeintroduzida no TypeScript 3.8 - TypeScript 5.0: verbatimModuleSyntax — Documentação da opção
verbatimModuleSyntaxque torna obrigatória a sintaxe explícita de imports de tipo - TypeScript ESLint: consistent-type-imports — Regra do ESLint para enforce automático de type-only imports em todo o projeto
- Webpack: Tree Shaking — Guia oficial do Webpack sobre como tree-shaking funciona e como type-only imports melhoram a eliminação de código morto
- Vite: TypeScript Features — Documentação do Vite sobre suporte a TypeScript, incluindo como lida com type-only imports durante o build
- esbuild: TypeScript Type-Only Imports — Explicação do esbuild sobre como trata imports de tipo e como eles são removidos durante a transpilação
- SWC: TypeScript Support — Documentação do SWC sobre suporte a TypeScript e otimização de type-only imports