Moduł w TypeScript to plik z co najmniej jednym import lub export na najwyższym poziomie. Wszystko, co jest w nim zadeklarowane, jest prywatne dla pliku, dopóki tego nie wyeksportujesz przez export, a inne pliki importują przez import to, czego potrzebują. Składnia to składnia ES modules z JavaScriptu plus kilka form tylko dla typów.
Eksporty nazwane i domyślne
Projekt ma wiele plików, więc strona importująca wygląda tak. Eksport domyślny importuje się bez nawiasów klamrowych i można nadać mu dowolną nazwę; eksporty nazwane idą w nawiasy klamrowe i zachowują swoje nazwy, chyba że zmienisz je przez as.
// 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);
| Forma | Co importuje |
|---|---|
import { a, b } from "./m.js" | eksporty nazwane a i b |
import x from "./m.js" | eksport domyślny pod nazwą x |
import * as m from "./m.js" | obiekt przestrzeni nazw ze wszystkimi eksportami |
import { a as b } from "./m.js" | a pod nazwą b w tym pliku |
import type { T } from "./m.js" | tylko typy, usuwane z wyjścia |
import "./setup.js" | uruchamia plik dla jego efektów ubocznych |
export { a } from "./m.js" | ponownie eksportuje a bez importowania |
export * from "./m.js" | ponownie eksportuje każdy eksport nazwany |
Ponowne eksporty pozwalają jednemu plikowi (często index.ts) zebrać publiczne API folderu. Zachowanie modułów w zwykłym JavaScripcie, takie jak live bindings i cache modułów, opisuje strona ES modules.
import type i export type
Typy nie istnieją w czasie działania, więc import używany tylko jako typ nie ma czego wczytać. import type mówi to wprost, a instrukcja znika z wyjściowego JavaScriptu. Modyfikator type działa też przy pojedynczej nazwie w zwykłym imporcie.
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"];
Bez tego słowa kluczowego TypeScript i tak usuwa nazwy, o których wie, że są tylko typami. Dwa ustawienia sprawiają, że słowo kluczowe jest wymagane:
verbatimModuleSyntax: truezachowuje każdy import, który nie jest oznaczonytype, więc nieoznaczony import typu to błąd TS1484:'Point' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.- Uruchamianie plików
.tsbezpośrednio w Node (type stripping) usuwa adnotacje typów, ale nie zagląda do innych plików. Nieoznaczoneimport { Point }zostaje w kodzie, a Node kończy się błędem w czasie działania:SyntaxError: The requested module './math.ts' does not provide an export named 'Point'.
Pisanie type przy każdym imporcie samych typów działa w obu przypadkach, więc warto wyrobić sobie ten nawyk.
Cannot find module
Gdy ścieżka w imporcie nie prowadzi do pliku ani do pakietu z typami, kompilator zatrzymuje się z błędem TS2307. Uruchom ten przykład, żeby go zobaczyć:
Wynik to index.ts(2,29): error TS2307: Cannot find module './utils.js' or its corresponding type declarations. Typowe przyczyny:
- Literówka w ścieżce względnej albo brak
./(bez niego nazwa jest szukana wnode_modules). - Pakiet JavaScript bez dołączonych typów: zainstaluj
@types/{package}, jeśli istnieje, albo napisz dla niego plik deklaracji. - Ścieżka podrzędna, której pakiet nie wymienia w polu
exportsswojegopackage.json. Przy rozwiązywaniunode16,nodenextibundlermożna importować tylko wymienione punkty wejścia.
Wyjście jako ES modules lub CommonJS
Zawsze piszesz import i export. Opcja module w tsconfig.json decyduje, jaki JavaScript powstaje.
// 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));
Przy node16, node18, node20 lub nodenext TypeScript stosuje dla każdego pliku reguły samego Node:
| Plik | Format wyjścia |
|---|---|
.ts w pakiecie z "type": "module" | ES module |
.ts w pakiecie bez tego pola | CommonJS |
.mts | zawsze ES module, generowany jako .mjs |
.cts | zawsze CommonJS, generowany jako .cjs |
Przy module: "esnext" lub "preserve" wyjście zachowuje import/export, czego oczekują bundlery takie jak Vite i esbuild. Różnice między tymi dwoma systemami modułów w czasie działania opisuje strona CommonJS a ESM.
Rozszerzenia plików w importach
Przy module: node16 lub nodenext plik ES module musi wskazywać plik, który Node faktycznie wczyta, a jest nim skompilowany plik .js. Do sprawdzania typów TypeScript mapuje ./math.js z powrotem na math.ts.
import { add } from "./math"; // error TS2835 in an ES module file
import { add } from "./math.js"; // correct: the path as it exists after compiling
Treść błędu to Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './math.js'? Pliki CommonJS przy tych samych ustawieniach mogą pomijać rozszerzenie, bo require sam próbuje rozszerzeń.
Dwie inne konfiguracje zmieniają tę regułę:
moduleResolution: "bundler"(zmoduleustawionym naesnext,preservelubcommonjs) akceptuje./mathbez rozszerzenia, bo rozwiązuje je bundler.rewriteRelativeImportExtensions: truepozwala pisać./math.ts, czyli tę samą ścieżkę, która działa, gdy Node uruchamia plik.tsbezpośrednio, i przepisuje ją na./math.jsw wyjściu.
Rozwiązywanie modułów i paths
Dla samej nazwy, takiej jak "zod", TypeScript szuka w node_modules, czyta package.json pakietu (pola exports i types), a w ostateczności sięga do node_modules/@types/zod. Nazwy względne (./, ../) są rozwiązywane względem pliku importującego. Można też zaimportować plik .json; potrzebne do tego ustawienia są na stronie JSON.
paths w tsconfig.json dodaje aliasy dla twoich własnych folderów:
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@lib/*": ["./src/lib/*"]
}
}
}
paths wpływa tylko na sprawdzanie typów. Wygenerowany plik nadal zawiera import { v } from "@lib/util", więc w czasie działania musi to rozwiązać coś innego: bundler skonfigurowany z tym samym aliasem albo pole imports Node w package.json (aliasy zaczynające się od #, jak "#lib/*": "./dist/lib/*"), które działa bez dodatkowych narzędzi. baseUrl nie istnieje w TypeScript 7 (błąd TS5102); zapisuj wpisy paths względem tsconfig.json, z ./ na początku.
Najczęściej zadawane pytania
Czym różni się import od import type w TypeScript?
import type { User } from "./user.js" może wczytać tylko typy, a cała instrukcja jest usuwana z wyjściowego JavaScriptu. Zwykły import może wczytać wartości i typy; TypeScript usuwa nazwy używane tylko jako typy, ale przy verbatimModuleSyntax wymaga oznaczenia ich przez type, żeby wyjście było dokładnie tym, co napisano.
Dlaczego TypeScript wymaga .js w ścieżkach importu?
Przy module: node16 lub nodenext plik ES module musi importować po prawdziwej nazwie pliku, który Node wczyta w czasie działania, a tym plikiem jest skompilowany .js. Podczas sprawdzania typów TypeScript rozwiązuje ./math.js do math.ts. Pominięcie rozszerzenia w pliku ES module to błąd TS2835.
Używać w TypeScript export default czy eksportów nazwanych?
Oba działają. Wiele zespołów woli eksporty nazwane: nazwa jest taka sama w każdym pliku, który ją importuje, edytory niezawodnie importują je automatycznie, a zmiana nazwy to refaktoryzacja, a nie wyszukiwanie. Eksport domyślny pozwala każdemu importującemu wybrać własną nazwę.
Czy opcja paths w tsconfig zmienia import w wyjściu?
Nie. paths mówi tylko modułowi sprawdzającemu typy, gdzie znaleźć moduł. Wygenerowany JavaScript zachowuje "@lib/util" w oryginalnej postaci, więc w czasie działania musi go rozwiązać bundler albo pole imports Node w package.json.
Jak naprawić "Cannot find module" w TypeScript?
Błąd TS2307 oznacza, że ścieżka nie prowadzi do pliku, który TypeScript może znaleźć, albo że pakiet nie dostarcza typów. Sprawdź ścieżkę względną i rozszerzenie, zainstaluj pakiet @types/..., jeśli pakiet nie ma wbudowanych typów, albo napisz dla niego mały plik deklaracji.