Configurando tsconfig para um novo projeto TypeScript
1. Visão geral
O arquivo tsconfig.json define como o TypeScript valida tipos e compila o código. Se mal configurado, você pode enfrentar erros de importação em tempo de execução ou o compilador deixar passar muitos erros de tipo.
Este artigo constrói um tsconfig.json para um novo projeto Node.js do zero. Vamos ativar as flags de validação rigorosa, escolher o sistema de módulos correto e separar a configuração de build da configuração de testes. Ao terminar, você terá um conjunto de configuração pronto para usar e entenderá cada linha nela.
2. Inicializando o projeto
Crie um diretório e instale TypeScript como uma dependência de desenvolvimento:
mkdir order-service && cd order-service
npm init -y
npm install --save-dev typescript @types/node
npx tsc --init
O comando tsc --init gera um tsconfig.json com muitas linhas de comentários. Este arquivo é útil para consulta, mas difícil de manter. Vamos substituí-lo por uma versão concisa, com cada opção claramente documentada.
Em seguida, declare que o projeto usa ES Modules no package.json:
{
"name": "order-service",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json",
"start": "node dist/index.js"
}
}
A linha "type": "module" diz ao Node.js que os arquivos .js são ESM. Essa escolha afeta diretamente como o TypeScript resolve os módulos, como você verá na seção 3.
3. Configuração mínima para Node.js
Este é o tsconfig.json base:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["node"],
"rootDir": ".",
"outDir": "dist",
"sourceMap": true,
"declaration": false,
"strict": true,
"skipLibCheck": true
},
"include": ["src", "tests"]
}
3.1. target, lib e types
targeté a versão de JavaScript que o compilador emite.ES2022é apropriado para Node.js 18+. Com esse target,async/await, optional chaining e class fields são preservados, não são degradados.libdeclara quais APIs estão disponíveis em tempo de execução. Sem"DOM", você não chamará acidentalmentedocumentouwindowno código do servidor.types: ["node"]carrega apenas os tipos de Node.js. Se não declarado, TypeScript carrega automaticamente todos os pacotes emnode_modules/@types. Isso às vezes traz tipos globais indesejados.
3.2. module e moduleResolution
Para um projeto que executa diretamente no Node.js, use NodeNext para ambas as opções. Neste modo, TypeScript simula corretamente como o Node.js resolve importações. Ele lê os campos "type" e "exports" em package.json e requer que importações relativas tenham extensões de arquivo.
Suponha que você tenha dois arquivos:
// src/order.ts
export interface Order {
id: string;
items: string[];
note?: string;
}
export function totalItems(order: Order): number {
return order.items.length;
}
// src/index.ts
import { totalItems } from "./order";
console.log(totalItems({ id: "A1", items: ["pho", "tra-da"] }));
Executar npx tsc --noEmit reportará um erro:
src/index.ts:1:28 - error TS2835: Relative import paths need explicit file extensions
in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'.
Did you mean './order.js'?
Você precisa corrigir para import { totalItems } from "./order.js";. A extensão é .js e não .ts, porque em tempo de execução o Node.js lê o arquivo compilado. Esta regra parece estranha à primeira vista, mas ajuda você a evitar o erro ERR_MODULE_NOT_FOUND ao fazer deploy.
3.3. rootDir e outDir
outDir é o local onde os arquivos .js são colocados após o build. rootDir determina a estrutura do diretório dentro de outDir. No arquivo base, rootDir é "." porque include tem tanto src quanto tests. Na seção 6, o arquivo de build mudará rootDir para src para que dist/index.js fique no lugar certo em vez de dist/src/index.js.
4. Strict mode e flags de validação adicionais
A flag strict: true ativa um grupo de validações, incluindo strictNullChecks, noImplicitAny e strictFunctionTypes. Para um novo projeto, você sempre deve ativar essa flag. Ativá-la posteriormente em uma base de código grande custará muito mais esforço.
Além de strict, você deve adicionar as seguintes flags:
{
"compilerOptions": {
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
Você mescla essas linhas no bloco compilerOptions do arquivo base. As duas primeiras flags merecem uma explicação mais detalhada.
4.1. noUncheckedIndexedAccess
Por padrão, o TypeScript assume que elementos acessados por índice sempre existem. Na prática, isso não é verdade:
import type { Order } from "./order.js";
const ordersById: Record<string, Order> = {};
const order = ordersById["A1"];
console.log(order.id);
Quando essa flag é ativada, o tipo de order é Order | undefined e o compilador reporta:
error TS18048: 'order' is possibly 'undefined'.
Você é forçado a verificar antes de usar: if (order) { console.log(order.id); }. Se você desativar a flag, o código acima ainda será compilado, mas lançará um TypeError em tempo de execução.
4.2. exactOptionalPropertyTypes
Esta flag distingue “propriedade ausente” de “propriedade com valor undefined”:
import type { Order } from "./order.js";
const order: Order = { id: "A2", items: [], note: undefined };
error TS2375: Type '{ id: string; items: never[]; note: undefined; }' is not assignable
to type 'Order' with 'exactOptionalPropertyTypes: true'.
Essa distinção é importante quando você usa Object.keys, o operador in ou serializa para JSON. Se você realmente quer permitir undefined, declare explicitamente: note?: string | undefined.
Nota:
exactOptionalPropertyTypesàs vezes entra em conflito com tipos de bibliotecas de terceiros, especialmente bibliotecas de manipulação de formulários e ORMs. Se essa flag causar muitos erros fora de seu controle, desative-a. MasnoUncheckedIndexedAccessdeve ser mantida.
5. Interoperabilidade entre módulos
Adicione as duas flags a seguir em compilerOptions:
{
"compilerOptions": {
"verbatimModuleSyntax": true,
"isolatedModules": true
}
}
verbatimModuleSyntax tem uma regra simples: qualquer import sem a palavra-chave type é mantido na saída, enquanto imports com type são removidos. Assim, o resultado da compilação é previsível. Em troca, você deve marcar explicitamente imports que são apenas para tipos:
// Errado: Order é apenas uma interface
import { Order } from "./order.js";
error TS1484: 'Order' is a type and must be imported using a type-only import
when 'verbatimModuleSyntax' is enabled.
// Correto
import type { Order } from "./order.js";
import { totalItems } from "./order.js";
isolatedModules garante que cada arquivo possa ser compilado independentemente. Ferramentas como esbuild, swc e Vitest processam um arquivo por vez, então essa flag ajuda você a detectar erros antes que eles causem problemas.
Você pode ter visto esModuleInterop em muitos guias antigos. Com module: NodeNext, essa flag é ativada por padrão, então você não precisa declarar explicitamente.
6. Separando configurações de typecheck e build
O arquivo tsconfig.json base serve para o editor e o comando typecheck. Inclui tests. Quando você faz build, não quer incluir arquivos de teste em dist. Portanto, crie um arquivo adicional tsconfig.build.json:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"noEmit": false
},
"include": ["src"],
"exclude": ["**/*.test.ts"]
}
extends herda todos os compilerOptions do arquivo base. No arquivo filho, include e exclude substituem completamente os valores do arquivo pai, eles não se mesclam. Execute o comando de build:
npm run build
ls dist
index.js index.js.map order.js order.js.map
O diretório dist contém apenas código da aplicação, colocado na raiz graças a rootDir: "src".
6.1. Escrevendo testes com Vitest
Instale o Vitest:
npm install --save-dev vitest
Adicione "test": "vitest run" ao scripts, então escreva testes para a função totalItems:
// tests/order.test.ts
import { describe, expect, it } from "vitest";
import type { Order } from "../src/order.js";
import { totalItems } from "../src/order.js";
describe("totalItems", () => {
it("conta corretamente o número de itens no pedido", () => {
const order: Order = { id: "A1", items: ["pho", "tra-da", "quay"] };
expect(totalItems(order)).toBe(3);
});
it("retorna 0 para um pedido vazio", () => {
expect(totalItems({ id: "A2", items: [] })).toBe(0);
});
});
npm run typecheck && npm test
✓ tests/order.test.ts (2 tests)
Test Files 1 passed (1)
Tests 2 passed (2)
O Vitest remove automaticamente tipos durante a execução sem verificá-los. Portanto, sempre execute typecheck no CI. Caso contrário, um teste com erro de tipo ainda pode “passar”.
7. Variante para projetos com bundler
Se seu código é empacotado por Vite, esbuild ou webpack em vez de executar diretamente no Node.js, altere a seção de módulos assim:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"jsx": "react-jsx"
}
}
moduleResolution: "Bundler"permite imports sem extensões de arquivo, já que o bundler faz a resolução.noEmit: truesignifica quetscapenas valida tipos, enquanto o bundler cuida de emitir os arquivos.- Adicione
"DOM"porque o código executa no navegador. - A linha
jsxé necessária apenas se você usar React.
Nota: Não use
Bundlerpara código que executa diretamente comnode dist/index.js. O compilador aceitará imports sem a extensão.js, mas o Node.js reportará um erro em tempo de execução.
8. Erros comuns
Usar paths para criar aliases e depois fazer build com tsc. Suponha que você declare "paths": { "@/*": ["src/*"] }. O editor entende esse alias, mas tsc não reescreve os caminhos na saída, então node não encontrará @/order.js. Para um projeto Node.js, use subpath imports em package.json:
{
"imports": {
"#src/*": "./dist/*"
}
}
Depois, você importa usando import { totalItems } from "#src/order.js";. No modo NodeNext, o TypeScript lê o campo imports e mapeia de volta de dist para src, então tanto o editor quanto o runtime resolvem corretamente.
Usar skipLibCheck em excesso. Essa flag apenas ignora a verificação de arquivos .d.ts, ajudando a compilar mais rápido e evitando erros dos tipos de bibliotecas. Isso não torna seu código menos seguro. No entanto, se você escrever seus próprios arquivos .d.ts, lembre-se que erros nesses próprios arquivos também serão ignorados.
Esquecer include. Sem include, o TypeScript varre todo o diretório, incluindo dist. Isso geralmente resulta no erro TS5055: Cannot write file ... because it would overwrite input file.
Misturar CommonJS e ESM. Se seu package.json não tem "type": "module" mas você usa NodeNext, seus arquivos .ts serão compilados para CommonJS. Nesse caso, importar um pacote que só suporta ESM causará um erro. Escolha uma direção desde o início. Para um novo projeto, escolha ESM.
Ativar strict e espalhando any por toda parte. Fazer isso anula o efeito do strict mode. Quando você não souber o tipo dos dados, use unknown e depois refine o tipo com verificações de tipo.
9. Conclusão
Este artigo construiu um tsconfig.json para um novo projeto Node.js com as seguintes partes:
moduleemoduleResolutiondefinidos comoNodeNext, juntamente com"type": "module"para que o compilador simule corretamente o runtime.strictjunto com as flagsnoUncheckedIndexedAccesseexactOptionalPropertyTypespara capturar erros cedo.verbatimModuleSyntaxeisolatedModulespara saída previsível e compatibilidade com ferramentas de compilação rápida.- Um arquivo
tsconfig.build.jsonseparado para quedistcontenha apenas código da aplicação.
Se seu projeto usa bundler, você apenas precisa mudar para moduleResolution: "Bundler" e noEmit: true. As flags de validação rigorosa permanecem iguais. Finalmente, sempre execute tsc --noEmit no CI, pois a maioria das ferramentas de teste e bundlers não verificam tipos para você.