Entendendo o tsconfig.json: Fundamentos Práticos
O tsconfig.json é o arquivo de configuração central do TypeScript. Ele define como o compilador deve se comportar, quais arquivos processar, para qual versão JavaScript compilar e como lidar com módulos. Muitos desenvolvedores o deixam com configurações padrão, perdendo controle fino sobre seu projeto. Dominar suas opções significa ter precisão na compilação, melhor compatibilidade e menos bugs em tempo de execução.
Quando você executa tsc sem argumentos, o TypeScript procura por tsconfig.json no diretório atual ou em diretórios pai. Se encontrado, usa suas configurações. Se não encontrado, usa padrões muito permissivos que aceitam quase qualquer código. Um bom tsconfig.json é como um contrato bem definido entre você e o compilador: você especifica regras estritas, o compilador as aplica rigorosamente.
Opções de Compilação Essenciais
Target e Lib: Definindo o Destino
A opção target define para qual versão do ECMAScript o TypeScript deve compilar. Se seu projeto precisa rodar em navegadores antigos, use ES5. Se é um projeto moderno, ES2020 ou superior faz sentido. A opção lib especifica quais bibliotecas de tipo estão disponíveis (DOM, ES6, etc.). Muitas vezes, você precisa ambas bem alinhadas.
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext"
}
}
No exemplo acima, estamos dizendo: compile para JavaScript moderno (ES2020), assume que teremos acesso às APIs do DOM e dos iteráveis, e use módulos ESNext (o compilador preservará a sintaxe import/export para um bundler lidar depois). Se seu projeto roda em Node.js puro, não inclua DOM na lib. Se precisa de funcionalidades específicas como Promise ou Proxy, garanta que estejam na lib.
Module: Qual Sistema de Módulos Usar
Existem vários sistemas de módulos: CommonJS (padrão Node.js antigo), ESNext (módulos modernos), UMD (compatível com múltiplos ambientes) e outros. A escolha depende do seu bundler e ambiente de execução.
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020"
}
}
Se usar Webpack, Vite ou Rollup, configure module: "ESNext" — esses bundlers entendem módulos ES6 nativamente e fazem otimizações melhores. Se seu código roda direto no Node.js sem bundler, use commonjs. Para bibliotecas NPM publicadas, muitas vezes você precisa gerar ambos: uma versão CommonJS (em dist/cjs) e uma ESNext (em dist/esm).
Strict: Ativando Segurança Total
A opção strict ativa um conjunto de verificações rigorosas simultaneamente. É como ligar todas as luzes de segurança de uma só vez.
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
Ao ativar strict: true, o TypeScript força você a ser explícito sobre tipos. Variáveis não podem ser any implícito, valores não podem ser null ou undefined sem declaração, e métodos precisam ser tipados corretamente. Isso evita toneladas de bugs. Se estiver herdando um projeto legado sem tipos, comece com strict: false e migre gradualmente para true.
Declaration e DeclarationMap: Gerando Tipos para Consumidores
Se você está criando uma biblioteca NPM, precisa fornecer informações de tipo para consumidores. A opção declaration gera arquivos .d.ts (arquivos de declaração). A opção declarationMap cria source maps para esses arquivos, permitindo navegar até o código original em IDEs.
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
Quando você compila com essas opções, TypeScript gera um .d.ts para cada .ts. Uma ferramenta como dts-bundle-generator depois consolida tudo em um único arquivo de tipo. Consumidores da sua biblioteca obterão IntelliSense perfeito. Sem declaration, sua biblioteca é praticamente invisível para usuários TypeScript.
Paths, Resolução de Módulos e BaseUrl
Configurando Aliases com Paths
Em projetos grandes, caminhos relativos como ../../../../utils/helper são ruins: difíceis de ler e quebram quando você move arquivos. A solução é usar aliases.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@types/*": ["src/types/*"]
}
}
}
Agora, em qualquer arquivo, você pode fazer:
import { Button } from "@components/button";
import { formatDate } from "@utils/date";
import type { User } from "@types/user";
Muito melhor. Seu bundler (Webpack, Vite, etc.) entende essas configurações automaticamente. Se estiver usando Jest para testes, configure moduleNameMapper no jest.config.js para que funcione lá também. TypeScript faz parte da história, mas não é toda.
Module Resolution: Node vs Classic
TypeScript suporta dois algoritmos de resolução de módulos. node segue a estratégia do Node.js (procura em node_modules, depois em diretórios pai). classic é o padrão antigo do TypeScript (quase obsoleto). Use sempre node.
{
"compilerOptions": {
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
}
}
A opção resolveJsonModule permite importar arquivos JSON diretamente (útil para configs). esModuleInterop corrige problemas de compatibilidade entre CommonJS e ESM. allowSyntheticDefaultImports permite sintaxe mais limpa ao importar CommonJS de código ESM.
Controle de Compilação e Diretórios
Include, Exclude e Files
TypeScript precisa saber quais arquivos compilar. Você controla isso com include, exclude e files. Use include para padrões (globs) e exclude para excluir caminhos específicos.
{
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"],
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
}
}
Este setup compila tudo em src, mas ignora testes (.spec.ts e .test.ts) e a pasta node_modules. Arquivos compilados vão para dist. Se usar files em vez de include, você lista arquivo por arquivo explicitamente — bom apenas para projetos muito pequenos.
OutDir, RootDir e Preservando Estrutura
rootDir é o diretório raiz da sua fonte, outDir é onde TypeScript coloca os arquivos compilados. TypeScript preserva a estrutura de pastas, removendo rootDir do caminho.
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"sourceMap": true,
"inlineSourceMap": false
}
}
Se você tiver src/components/Button.ts, ele será compilado para dist/components/Button.js. A opção sourceMap gera arquivos .map que mapeiam o código compilado de volta ao TypeScript original — essencial para debugar em produção. inlineSourceMap embuça o source map no próprio JavaScript (útil para ambientes onde o arquivo .map não está disponível, mas aumenta o tamanho do arquivo).
Skiplib e SkipDefaults
skipLibCheck faz TypeScript não verificar tipos de bibliotecas da node_modules. Economiza tempo de compilação massivamente (até 50% em alguns projetos). Se uma biblioteca tem tipos ruins, você não quer descobrir durante sua compilação.
{
"compilerOptions": {
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true
}
}
A opção forceConsistentCasingInFileNames evita problemas em sistemas de arquivos case-sensitive (Linux/Mac) onde Button.ts e button.ts são arquivos diferentes — comum quando desenvolve em Windows e deploy em Linux.
Segurança de Tipos Avançada
NoUnusedLocals e NoUnusedParameters
Essas opções forçam você a remover código morto, mantendo o projeto limpo e focado.
function calculateTotal(items: Item[], tax: number, discount: number) {
// ❌ Com noUnusedParameters: true, discount causaria erro
return items.reduce((sum, item) => sum + item.price, 0) * (1 + tax);
}
Com essas opções ativadas, qualquer variável ou parâmetro não usado gera um erro. Você é forçado a removê-lo ou usar. Parece chato, mas mantém seu código refatorado constantemente.
{
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
noImplicitReturns garante que todas as branches de uma função retornem algo. noFallthroughCasesInSwitch impede falls-through acidentais em switches sem break.
Experimentais: useDefineForClassFields
Quando seu código compila classes, TypeScript tem duas estratégias: uma segue o padrão JavaScript moderno (usando getters/setters definidos com Object.defineProperty), outra mais simples. A opção useDefineForClassFields usa a estratégia moderna.
class User {
name: string = "Unknown"; // Com useDefineForClassFields: true, comportamento diferente
constructor(name: string) {
this.name = name;
}
}
Ative isso se seu target é ES2022 ou superior. Evita problemas com herança e property descriptors.
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true
}
}
Exemplo Prático Completo
Aqui está um tsconfig.json realista e bem construído para um projeto full-stack moderno:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@hooks/*": ["src/hooks/*"],
"@utils/*": ["src/utils/*"],
"@types/*": ["src/types/*"]
},
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"moduleResolution": "node",
"useDefineForClassFields": true,
"isolatedModules": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"],
"ts-node": {
"compilerOptions": {
"module": "commonjs"
}
}
}
Neste setup: você compila para ES2020 (moderno), usa paths para organizar imports, ativa verificações estritas completas, gera tipos para consumidores, ignora bibliotecas em type-checking rápido com skipLibCheck, e permite que ts-node rode testes em CommonJS (compatível com Jest). É profissional, robusto e escalável.
Conclusão
Três lições essenciais sobre tsconfig.json: Primeiro, opções como strict, noUnusedLocals e noImplicitReturns não são detalhes — elas definem a qualidade do seu código. Ativá-las cedo previne bugs massivos depois. Segundo, baseUrl e paths transformam a legibilidade de projetos grandes — investir tempo aqui vale a pena. Terceiro, entender target, lib, module e moduleResolution é crucial quando seu código precisa rodar em ambientes diferentes (navegador, Node.js, bundlers variados). Não deixe essas configurações ao acaso.