Ein TypeScript-Modul ist eine Datei mit mindestens einem import oder export auf oberster Ebene. Alles, was darin deklariert wird, ist privat für die Datei, solange du es nicht mit export freigibst, und andere Dateien holen sich mit import, was sie brauchen. Die Syntax ist die ES-Modul-Syntax von JavaScript plus einige Formen nur für Typen.
Benannte und Default-Exports
Ein Projekt hat viele Dateien, die importierende Seite sieht also so aus. Der Default-Export wird ohne geschweifte Klammern importiert und kann jeden Namen bekommen; benannte Exports stehen in geschweiften Klammern und behalten ihre Namen, außer du benennst sie mit as um.
// main.ts
import describe, { distance, ORIGIN, type Point } from "./math.js";
import { distance as dist } from "./math.js"; // renamed on import
import * as math from "./math.js"; // everything, as one object
const p: Point = { x: 6, y: 8 };
console.log(describe(p), distance(ORIGIN, p), dist(p, p), math.ORIGIN);
| Form | Was sie importiert |
|---|---|
import { a, b } from "./m.js" | die benannten Exports a und b |
import x from "./m.js" | den Default-Export unter dem Namen x |
import * as m from "./m.js" | ein Namespace-Objekt mit allen Exports |
import { a as b } from "./m.js" | a, in dieser Datei in b umbenannt |
import type { T } from "./m.js" | nur Typen, aus der Ausgabe entfernt |
import "./setup.js" | führt die Datei wegen ihrer Nebeneffekte aus |
export { a } from "./m.js" | exportiert a weiter, ohne es zu importieren |
export * from "./m.js" | exportiert jeden benannten Export weiter |
Mit Re-Exports sammelt eine Datei (oft index.ts) die öffentliche API eines Ordners. Das Verhalten reiner JavaScript-Module, etwa Live Bindings und Modul-Caching, wird unter ES-Module erklärt.
import type und export type
Typen existieren zur Laufzeit nicht, also gibt es bei einem Import, der nur als Typ verwendet wird, nichts zu laden. import type sagt das ausdrücklich, und die Anweisung verschwindet aus der JavaScript-Ausgabe. Der Modifikator type funktioniert auch für einen einzelnen Namen in einem normalen Import.
import type { User } from "./models.js"; // whole statement erased
import { saveUser, type Settings } from "./api.js"; // only saveUser survives
export type { User }; // re-export a type only
export type UserId = User["id"];
Auch ohne das Schlüsselwort entfernt TypeScript Namen, von denen es sieht, dass sie nur Typen sind. Zwei Einstellungen machen das Schlüsselwort zur Pflicht:
verbatimModuleSyntax: truebehält jeden Import, der nicht mittypemarkiert ist, ein unmarkierter Typimport ist also der Fehler TS1484:'Point' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.- Werden
.ts-Dateien direkt in Node ausgeführt (Type Stripping), entfernt Node Typannotationen, schaut aber nicht in andere Dateien. Ein unmarkiertesimport { Point }bleibt im Code, und Node scheitert zur Laufzeit mitSyntaxError: The requested module './math.ts' does not provide an export named 'Point'.
type bei jedem reinen Typimport zu schreiben funktioniert in beiden Fällen, daher lohnt es sich, das zur Gewohnheit zu machen.
Cannot Find Module
Führt der Pfad in einem Import zu keiner Datei und keinem Paket mit Typen, bricht der Compiler mit dem Fehler TS2307 ab. Führe diesen Block aus, um ihn zu sehen:
Die Ausgabe ist index.ts(2,29): error TS2307: Cannot find module './utils.js' or its corresponding type declarations. Die üblichen Ursachen:
- Ein Tippfehler in einem relativen Pfad oder ein fehlendes
./(ohne das wird der Name innode_modulesgesucht). - Ein JavaScript-Paket ohne mitgelieferte Typen: Installiere
@types/{package}, falls es existiert, oder schreibe eine Deklarationsdatei dafür. - Ein Unterpfad, den das Paket nicht im Feld
exportsseinerpackage.jsonaufführt. Unter der Auflösungnode16,nodenextundbundlerlassen sich nur aufgeführte Einstiegspunkte importieren.
ES-Module oder CommonJS als Ausgabe
Du schreibst immer import und export. Die Option module in tsconfig.json entscheidet, welches JavaScript herauskommt.
// Source, the same in both cases
import { add } from "./math.js";
console.log(add(1, 2));
// module: nodenext, in a package with "type": "module"
import { add } from "./math.js";
console.log(add(1, 2));
// module: nodenext, in a package without "type": "module"
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const math_js_1 = require("./math.js");
console.log((0, math_js_1.add)(1, 2));
Mit node16, node18, node20 oder nodenext folgt TypeScript für jede Datei den Regeln von Node:
| Datei | Ausgabeformat |
|---|---|
.ts in einem Paket mit "type": "module" | ES-Modul |
.ts in einem Paket ohne diese Angabe | CommonJS |
.mts | immer ES-Modul, ausgegeben als .mjs |
.cts | immer CommonJS, ausgegeben als .cjs |
Mit module: "esnext" oder "preserve" behält die Ausgabe import/export, was Bundler wie Vite und esbuild erwarten. Die Unterschiede der beiden Modulsysteme zur Laufzeit stehen unter CommonJS vs ESM.
Dateiendungen in Imports
Unter module: node16 oder nodenext muss eine ES-Modul-Datei die Datei nennen, die Node tatsächlich lädt, und das ist die kompilierte .js-Datei. TypeScript bildet ./math.js für die Typprüfung zurück auf math.ts ab.
import { add } from "./math"; // error TS2835 in an ES module file
import { add } from "./math.js"; // correct: the path as it exists after compiling
Der Fehlertext lautet Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './math.js'? CommonJS-Dateien dürfen bei denselben Einstellungen die Endung weglassen, weil require Endungen selbst ausprobiert.
Zwei andere Konfigurationen ändern die Regel:
moduleResolution: "bundler"(mitmoduleaufesnext,preserveodercommonjs) akzeptiert./mathohne Endung, da der Bundler es auflöst.rewriteRelativeImportExtensions: trueerlaubt dir,./math.tszu schreiben, also denselben Pfad, der funktioniert, wenn Node die.ts-Datei direkt ausführt, und schreibt ihn in der Ausgabe zu./math.jsum.
Modulauflösung und paths
Bei einem nackten Namen wie "zod" sucht TypeScript in node_modules, liest die package.json des Pakets (die Felder exports und types) und greift ersatzweise auf node_modules/@types/zod zurück. Relative Namen (./, ../) werden von der importierenden Datei aus aufgelöst. Auch eine .json-Datei lässt sich importieren; die dafür nötigen Einstellungen stehen auf der Seite zu JSON.
paths in tsconfig.json fügt Aliasse für deine eigenen Ordner hinzu:
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@lib/*": ["./src/lib/*"]
}
}
}
paths wirkt nur auf die Typprüfung. In der erzeugten Datei steht weiterhin import { v } from "@lib/util", also muss etwas anderes das zur Laufzeit auflösen: ein Bundler mit demselben Alias oder das Feld imports in der package.json von Node (Aliasse, die mit # beginnen, wie "#lib/*": "./dist/lib/*"), das ohne zusätzliches Tool funktioniert. baseUrl gibt es in TypeScript 7 nicht mehr (Fehler TS5102); schreibe die Einträge in paths relativ zur tsconfig.json mit einem führenden ./.
Häufig gestellte Fragen
Was ist der Unterschied zwischen import und import type in TypeScript?
import type { User } from "./user.js" kann nur Typen importieren, und die ganze Anweisung wird aus der JavaScript-Ausgabe entfernt. Ein normales import kann Werte und Typen importieren; TypeScript lässt die Namen weg, die nur als Typen verwendet werden, aber mit verbatimModuleSyntax musst du diese mit type markieren, damit die Ausgabe genau dem entspricht, was du geschrieben hast.
Warum will TypeScript .js in Importpfaden?
Unter module: node16 oder nodenext muss eine ES-Modul-Datei mit dem echten Dateinamen importieren, den Node zur Laufzeit lädt, und das ist die kompilierte .js-Datei. TypeScript löst ./math.js bei der Typprüfung zu math.ts auf. Die Endung in einer ES-Modul-Datei wegzulassen ist der Fehler TS2835.
Sollte ich in TypeScript export default oder benannte Exports verwenden?
Beides funktioniert. Viele Teams bevorzugen benannte Exports: Der Name ist in jeder importierenden Datei derselbe, Editoren importieren sie zuverlässig automatisch, und Umbenennen ist ein Refactoring statt einer Suche. Bei einem Default-Export wählt jeder Importierende seinen eigenen Namen.
Ändert die Option paths in der tsconfig den Import in der Ausgabe?
Nein. paths sagt nur der Typprüfung, wo ein Modul zu finden ist. Das erzeugte JavaScript behält "@lib/util" wie geschrieben, also muss ein Bundler oder das Feld imports in der package.json von Node es zur Laufzeit auflösen.
Wie behebe ich den Fehler "Cannot find module" in TypeScript?
Der Fehler TS2307 bedeutet, dass der Pfad auf keine Datei zeigt, die TypeScript findet, oder dass ein Paket keine Typen mitliefert. Prüfe den relativen Pfad und die Endung, installiere das Paket @types/..., wenn es keine eingebauten Typen hat, oder schreibe eine kleine Deklarationsdatei dafür.