tsconfig für ein neues TypeScript-Projekt
1. Überblick
Die Datei tsconfig.json bestimmt, wie TypeScript Typen prüft und Code kompiliert. Bei falscher Konfiguration können Sie Importfehler zur Laufzeit oder unbemerkte Typfehler bekommen.
Dieser Artikel baut eine tsconfig.json für ein neues Node.js-Projekt von Grund auf auf. Wir werden strikte Prüfflags aktivieren, das richtige Modulsystem wählen und die Build-Konfiguration von der Test-Konfiguration trennen. Nach dem Lesen werden Sie eine sofort einsatzbereite Konfiguration haben und jede Zeile darin verstehen.
2. Projekt initialisieren
Erstellen Sie ein Verzeichnis und installieren Sie TypeScript als Entwicklungsabhängigkeit:
mkdir order-service && cd order-service
npm init -y
npm install --save-dev typescript @types/node
npx tsc --init
Der Befehl tsc --init erzeugt eine tsconfig.json mit vielen Kommentarzeilen. Diese Datei ist zum Nachschlagen nützlich, aber schwer zu warten. Wir werden sie durch eine prägnante Version ersetzen, die jede Option klar dokumentiert.
Deklarieren Sie anschließend in package.json, dass das Projekt ES Module verwendet:
{
"name": "order-service",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc -p tsconfig.build.json",
"start": "node dist/index.js"
}
}
Die Zeile "type": "module" teilt Node.js mit, dass die .js-Dateien ESM sind. Diese Wahl beeinflusst direkt, wie TypeScript Module auflöst, wie Sie in Abschnitt 3 sehen werden.
3. Minimale Konfiguration für Node.js
Hier ist die Basis-tsconfig.json:
{
"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 und types
targetist die JavaScript-Version, die der Compiler ausgibt.ES2022passt zu Node.js 18 und neuer. Bei diesem Ziel werdenasync/await, optional chaining und class fields unverändert beibehalten, nicht herabgestuft.libdeklariert die verfügbaren APIs zur Laufzeit. Ohne"DOM"können Sie versehentlich nichtdocumentoderwindowin Server-Code aufrufen.types: ["node"]lädt nur Node.js-Typen. Ohne diese Deklaration lädt TypeScript automatisch alle Pakete innode_modules/@types. Dies zieht manchmal unerwünschte globale Typen mit sich.
3.2. module und moduleResolution
Bei einem Projekt, das direkt auf Node.js läuft, verwenden Sie NodeNext für beide Optionen. In diesem Modus simuliert TypeScript genau, wie Node.js Module auflöst. Es liest die Felder "type" und "exports" in package.json und erfordert, dass relative Importe eine Dateierweiterung haben.
Angenommen, Sie haben zwei Dateien:
// 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"] }));
Das Ausführen von npx tsc --noEmit meldet einen Fehler:
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'?
Sie müssen es zu import { totalItems } from "./order.js"; ändern. Die Erweiterung ist .js statt .ts, weil Node.js zur Laufzeit die kompilierte Datei liest. Diese Regel wirkt anfangs seltsam, aber sie hilft Ihnen, den Fehler ERR_MODULE_NOT_FOUND bei der Bereitstellung zu vermeiden.
3.3. rootDir und outDir
outDir ist der Ort, wo TypeScript die .js-Dateien nach dem Build ablegt. rootDir bestimmt die Verzeichnisstruktur innerhalb von outDir. In der Basis-Datei ist rootDir ".", da include sowohl src als auch tests enthält. In Abschnitt 6 wird die Build-Datei rootDir in src ändern, damit dist/index.js am richtigen Ort liegt, statt bei dist/src/index.js.
4. Strict Mode und zusätzliche Prüfflags
Der Flag strict: true aktiviert eine Gruppe von Prüfungen, darunter strictNullChecks, noImplicitAny und strictFunctionTypes. Bei einem neuen Projekt sollten Sie diesen Flag immer aktivieren. Es später für eine große Codebasis zu aktivieren, wird viel mehr Aufwand erfordern.
Zusätzlich zu strict sollten Sie diese Flags hinzufügen:
{
"compilerOptions": {
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
Sie müssen diese Zeilen in den compilerOptions-Block der Basis-Datei einfügen. Die ersten beiden Flags verdienen eine genauere Erklärung.
4.1. noUncheckedIndexedAccess
Standardmäßig geht TypeScript davon aus, dass ein per Index abgerufenes Element immer existiert. Das stimmt aber nicht:
import type { Order } from "./order.js";
const ordersById: Record<string, Order> = {};
const order = ordersById["A1"];
console.log(order.id);
Wenn Sie diesen Flag aktivieren, ist der Typ von order Order | undefined, und der Compiler meldet:
error TS18048: 'order' is possibly 'undefined'.
Sie müssen vor der Verwendung prüfen: if (order) { console.log(order.id); }. Wenn Sie den Flag ausschalten, kompiliert der obige Code trotzdem, aber wirft einen TypeError zur Laufzeit.
4.2. exactOptionalPropertyTypes
Dieser Flag unterscheidet “Eigenschaft nicht vorhanden” von “Eigenschaft mit dem Wert 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'.
Diese Unterscheidung ist wichtig, wenn Sie Object.keys, den Operator in oder Serialisierung zu JSON verwenden. Wenn Sie wirklich undefined erlauben möchten, deklarieren Sie es explizit: note?: string | undefined.
Hinweis:
exactOptionalPropertyTypesverursacht manchmal Konflikte mit Typen von Drittanbieter-Bibliotheken, besonders bei Bibliotheken für Formulare und ORMs. Wenn dieser Flag zu viele Fehler außerhalb Ihrer Kontrolle verursacht, schalten Sie ihn aus. AbernoUncheckedIndexedAccesssollten Sie behalten.
5. Interaktion zwischen Modulen
Fügen Sie diese zwei Flags in compilerOptions ein:
{
"compilerOptions": {
"verbatimModuleSyntax": true,
"isolatedModules": true
}
}
verbatimModuleSyntax hat eine einfache Regel: Ein Import ohne type wird in der Ausgabe beibehalten, ein Import mit type wird gelöscht. So ist das Kompilierungsergebnis vorhersehbar. Im Gegenzug müssen Sie Importe explizit kennzeichnen, die nur für Typen verwendet werden:
// Falsch: Order ist nur eine 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.
// Richtig
import type { Order } from "./order.js";
import { totalItems } from "./order.js";
isolatedModules stellt sicher, dass jede Datei isoliert kompiliert werden kann. Tools wie esbuild, swc oder Vitest verarbeiten jede Datei einzeln, daher hilft dieser Flag, Fehler zu finden, bevor sie Probleme verursachen.
Sie haben möglicherweise esModuleInterop in vielen älteren Anleitungen gesehen. Mit module: NodeNext ist dieser Flag standardmäßig aktiviert, daher müssen Sie ihn nicht zusätzlich deklarieren.
6. Build- und Test-Konfiguration trennen
Die tsconfig.json Basis-Datei dient dem Editor und dem typecheck-Befehl. Sie enthält auch tests. Beim Build möchten Sie Test-Dateien nicht in dist aufnehmen. Erstellen Sie daher eine zusätzliche Datei tsconfig.build.json:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"noEmit": false
},
"include": ["src"],
"exclude": ["**/*.test.ts"]
}
extends erbt alle compilerOptions aus der Basis-Datei. In der untergeordneten Datei ersetzen include und exclude die Werte der übergeordneten Datei vollständig, statt sich zu verbinden. Führen Sie den Build-Befehl aus:
npm run build
ls dist
index.js index.js.map order.js order.js.map
Das dist-Verzeichnis enthält nur Anwendungscode, dank rootDir: "src" direkt in der Wurzel.
6.1. Tests mit Vitest schreiben
Installieren Sie Vitest:
npm install --save-dev vitest
Fügen Sie "test": "vitest run" zu scripts hinzu, dann schreiben Sie einen Test für die Funktion 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("zählt korrekt die Anzahl der Gerichte in einer Bestellung", () => {
const order: Order = { id: "A1", items: ["pho", "tra-da", "quay"] };
expect(totalItems(order)).toBe(3);
});
it("gibt 0 für eine leere Bestellung zurück", () => {
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)
Vitest entfernt automatisch Typen während der Ausführung, ohne sie zu überprüfen. Führen Sie daher immer typecheck in CI aus. Andernfalls kann ein Test mit Typfehlern immer noch “bestehen”.
7. Variante für Projekte mit Bundler
Wenn der Code durch Vite, esbuild oder webpack statt direkt auf Node.js verpackt wird, ändern Sie den Modul-Teil wie folgt:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"jsx": "react-jsx"
}
}
moduleResolution: "Bundler"erlaubt Importe ohne Dateierweiterung, weil der Bundler die Auflösung selbst übernimmt.noEmit: truebedeutet, dasstscnur Typen überprüft, der Bundler kümmert sich um das Exportieren von Dateien.- Fügen Sie
"DOM"hinzu, weil der Code im Browser läuft. Die Zeilejsxist nur notwendig, wenn Sie React verwenden.
Hinweis: Verwenden Sie nicht
Bundlerfür Code, der direkt mitnode dist/index.jsausgeführt wird. Der Compiler akzeptiert Importe ohne.js-Erweiterung, aber Node.js meldet einen Fehler zur Laufzeit.
8. Häufige Fehler
Verwendung von paths für Aliase, dann Kompilierung mit tsc. Angenommen, Sie deklarieren "paths": { "@/*": ["src/*"] }. Der Editor versteht diese Aliase, aber tsc schreibt die Pfade in der Ausgabe nicht um, daher kann node @/order.js nicht finden. Verwenden Sie bei Node.js-Projekten stattdessen Subpath-Importe in package.json:
{
"imports": {
"#src/*": "./dist/*"
}
}
Dann importieren Sie mit import { totalItems } from "#src/order.js";. Im Modus NodeNext liest TypeScript das Feld imports und bildet das Mapping von dist zu src um, daher lösen Editor und Runtime korrekt auf.
Missbrauch von skipLibCheck. Dieser Flag überspringt nur die Überprüfung von .d.ts-Dateien, was den Build schneller macht und Fehler von Typangaben anderer Bibliotheken vermeidet. Es macht Ihren Code nicht weniger sicher. Wenn Sie jedoch selbst .d.ts-Dateien schreiben, denken Sie daran, dass Fehler in diesen Dateien ebenfalls übersehen werden.
Vergessen von include. Wenn include nicht vorhanden ist, durchsucht TypeScript das gesamte Verzeichnis, einschließlich dist. Das Ergebnis ist häufig der Fehler TS5055: Cannot write file ... because it would overwrite input file.
Mischung aus CommonJS und ESM. Wenn package.json nicht "type": "module" hat, aber Sie trotzdem NodeNext verwenden, werden .ts-Dateien zu CommonJS kompiliert. In diesem Fall führt das Importieren eines Pakets, das nur ESM unterstützt, zu Fehlern. Wählen Sie eine Richtung von Anfang an. Wählen Sie für neue Projekte ESM.
strict aktivieren und dann any überall verteilen. Das macht den Strict Mode nutzlos. Wenn Sie den Typ von Daten nicht kennen, verwenden Sie unknown und verengen Sie dann den Typ mit Überprüfungsanweisungen.
9. Fazit
Der Artikel hat eine tsconfig.json für ein neues Node.js-Projekt mit folgenden Teilen aufgebaut:
moduleundmoduleResolutionaufNodeNextgesetzt, zusammen mit"type": "module", damit der Compiler die Runtime korrekt simuliert.strictsowie die FlagsnoUncheckedIndexedAccessundexactOptionalPropertyTypeszum frühen Erkennen von Fehlern.verbatimModuleSyntaxundisolatedModules, um die Ausgabe vorhersehbar zu machen und mit schnellen Compiler-Tools kompatibel zu sein.- Eine separate Datei
tsconfig.build.json, damitdistnur Anwendungscode enthält.
Wenn das Projekt einen Bundler verwendet, müssen Sie nur zu moduleResolution: "Bundler" und noEmit: true wechseln. Die strikten Prüfflags bleiben gleich. Führen Sie schließlich immer tsc --noEmit in CI aus, da die meisten Test-Runner und Bundler Typen nicht für Sie überprüfen.