Menu

TypeScript Module: import, export und import type

Jede TypeScript-Datei mit einem import oder export auf oberster Ebene ist ein Modul. Benannte und Default-Exports, import type und export type, wie die Einstellung module zwischen ES-Modul und CommonJS-Ausgabe entscheidet und warum node16 und nodenext .js-Endungen in Imports verlangen.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

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);
FormWas 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: true behält jeden Import, der nicht mit type markiert 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 unmarkiertes import { Point } bleibt im Code, und Node scheitert zur Laufzeit mit SyntaxError: 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 in node_modules gesucht).
  • 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 exports seiner package.json aufführt. Unter der Auflösung node16, nodenext und bundler lassen 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:

DateiAusgabeformat
.ts in einem Paket mit "type": "module"ES-Modul
.ts in einem Paket ohne diese AngabeCommonJS
.mtsimmer ES-Modul, ausgegeben als .mjs
.ctsimmer 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" (mit module auf esnext, preserve oder commonjs) akzeptiert ./math ohne Endung, da der Bundler es auflöst.
  • rewriteRelativeImportExtensions: true erlaubt dir, ./math.ts zu schreiben, also denselben Pfad, der funktioniert, wenn Node die .ts-Datei direkt ausführt, und schreibt ihn in der Ausgabe zu ./math.js um.

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.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S