Menu

Moduły w TypeScript: import, export i import type

Każdy plik TypeScript z importem lub eksportem na najwyższym poziomie jest modułem. Poznaj eksporty nazwane i domyślne, import type i export type, to, jak opcja module decyduje o wyjściu jako ES module lub CommonJS, i dlaczego node16 i nodenext wymagają rozszerzeń .js w importach.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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);
FormaCo 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: true zachowuje każdy import, który nie jest oznaczony type, 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 .ts bezpośrednio w Node (type stripping) usuwa adnotacje typów, ale nie zagląda do innych plików. Nieoznaczone import { 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 w node_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 exports swojego package.json. Przy rozwiązywaniu node16, nodenext i bundler moż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:

PlikFormat wyjścia
.ts w pakiecie z "type": "module"ES module
.ts w pakiecie bez tego polaCommonJS
.mtszawsze ES module, generowany jako .mjs
.ctszawsze 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" (z module ustawionym na esnext, preserve lub commonjs) akceptuje ./math bez rozszerzenia, bo rozwiązuje je bundler.
  • rewriteRelativeImportExtensions: true pozwala pisać ./math.ts, czyli tę samą ścieżkę, która działa, gdy Node uruchamia plik .ts bezpośrednio, i przepisuje ją na ./math.js w 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ