Un modulo TypeScript è un file con almeno un import o un export al livello più alto. Tutto ciò che vi è dichiarato resta privato del file a meno che tu non lo esporti con export, e gli altri file importano con import ciò che serve loro. La sintassi è quella degli ES module di JavaScript più qualche forma riservata ai tipi.
Export con nome e di default
Un progetto ha molti file, quindi il lato che importa è fatto così. L'export di default si importa senza graffe e può prendere qualsiasi nome; gli export con nome vanno tra graffe e mantengono il loro nome, a meno che tu non li rinomini con 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 | Cosa importa |
|---|---|
import { a, b } from "./m.js" | gli export con nome a e b |
import x from "./m.js" | l'export di default, con il nome x |
import * as m from "./m.js" | un oggetto namespace che contiene ogni export |
import { a as b } from "./m.js" | a, rinominato b in questo file |
import type { T } from "./m.js" | solo tipi, rimossi dall'output |
import "./setup.js" | esegue il file per i suoi effetti collaterali |
export { a } from "./m.js" | riesporta a senza importarlo |
export * from "./m.js" | riesporta ogni export con nome |
Le riesportazioni permettono a un file (spesso index.ts) di raccogliere l'API pubblica di una cartella. Il comportamento dei moduli nel JavaScript puro, come i live binding e la cache dei moduli, è trattato in ES modules.
import type ed export type
I tipi non esistono a runtime, quindi un import usato solo come tipo non ha niente da caricare. import type lo dice esplicitamente, e l'istruzione sparisce dall'output JavaScript. Il modificatore type funziona anche su un singolo nome dentro un import normale.
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"];
Senza la parola chiave, TypeScript rimuove comunque i nomi che riconosce come solo tipi. Due impostazioni rendono la parola chiave obbligatoria:
verbatimModuleSyntax: truemantiene ogni import non marcato contype, quindi un import di tipo non marcato è l'errore TS1484:'Point' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.- Eseguire i file
.tsdirettamente in Node (type stripping) rimuove le annotazioni di tipo ma non guarda gli altri file. Unimport { Point }non marcato resta nel codice, e Node fallisce a runtime conSyntaxError: The requested module './math.ts' does not provide an export named 'Point'.
Scrivere type su ogni import di soli tipi funziona in entrambi i casi, quindi è l'abitudine da prendere.
Cannot find module
Quando il percorso di un import non porta a un file o a un pacchetto con tipi, il compilatore si ferma con l'errore TS2307. Esegui questo per vederlo:
L'output è index.ts(2,29): error TS2307: Cannot find module './utils.js' or its corresponding type declarations. Le cause più comuni:
- Un errore di battitura in un percorso relativo, o un
./mancante (senza, il nome viene cercato innode_modules). - Un pacchetto JavaScript senza tipi inclusi: installa
@types/{package}se esiste, oppure scrivi un file di dichiarazione. - Un sottopercorso che il pacchetto non elenca nel campo
exportsdel suopackage.json. Con la risoluzionenode16,nodenextebundler, si possono importare solo i punti di ingresso elencati.
Output ES module o CommonJS
Scrivi sempre import ed export. È l'opzione module in tsconfig.json a decidere quale JavaScript ne esce.
// 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));
Con node16, node18, node20 o nodenext, TypeScript segue le regole di Node per ogni file:
| File | Formato dell'output |
|---|---|
.ts in un pacchetto con "type": "module" | ES module |
.ts in un pacchetto senza | CommonJS |
.mts | sempre ES module, generato come .mjs |
.cts | sempre CommonJS, generato come .cjs |
Con module: "esnext" o "preserve", l'output mantiene import/export, che è ciò che si aspettano bundler come Vite ed esbuild. Le differenze tra i due sistemi di moduli a runtime sono in CommonJS vs ESM.
Estensioni dei file negli import
Con module: node16 o nodenext, un file ES module deve indicare il file che Node caricherà davvero, cioè il file .js compilato. TypeScript riporta ./math.js a math.ts per il controllo dei tipi.
import { add } from "./math"; // error TS2835 in an ES module file
import { add } from "./math.js"; // correct: the path as it exists after compiling
Il testo dell'errore è Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './math.js'? Con le stesse impostazioni, i file CommonJS possono omettere l'estensione, perché require prova le estensioni da solo.
Altre due configurazioni cambiano la regola:
moduleResolution: "bundler"(conmoduleimpostato aesnext,preserveocommonjs) accetta./mathsenza estensione, perché è il bundler a risolverlo.rewriteRelativeImportExtensions: trueti permette di scrivere./math.ts, lo stesso percorso che funziona quando Node esegue direttamente il file.ts, e lo riscrive in./math.jsnell'output.
Risoluzione dei moduli e paths
Per un nome semplice come "zod", TypeScript cerca in node_modules, legge il package.json del pacchetto (i campi exports e types) e, in mancanza, ripiega su node_modules/@types/zod. I nomi relativi (./, ../) si risolvono a partire dal file che importa. Si può importare anche un file .json; le impostazioni necessarie sono nella pagina su JSON.
paths in tsconfig.json aggiunge degli alias per le tue cartelle:
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@lib/*": ["./src/lib/*"]
}
}
}
paths influisce solo sul controllo dei tipi. Il file generato dice ancora import { v } from "@lib/util", quindi a runtime qualcos'altro deve risolverlo: un bundler configurato con lo stesso alias, oppure il campo imports di Node nel package.json (alias che iniziano con #, come "#lib/*": "./dist/lib/*"), che funziona senza strumenti aggiuntivi. baseUrl non esiste più in TypeScript 7 (errore TS5102); scrivi le voci di paths relative al tsconfig.json con un ./ iniziale.
Domande frequenti
Che differenza c'è tra import e import type in TypeScript?
import type { User } from "./user.js" può portare dentro solo tipi, e l'intera istruzione viene rimossa dall'output JavaScript. Un import normale può portare valori e tipi; TypeScript scarta i nomi usati solo come tipi, ma con verbatimModuleSyntax ti chiede di marcarli con type, così l'output è esattamente quello che hai scritto.
Perché TypeScript vuole .js nei percorsi degli import?
Con module: node16 o nodenext, un file ES module deve importare usando il vero nome del file che Node caricherà a runtime, e quel file è il .js compilato. Durante il controllo dei tipi TypeScript risolve ./math.js in math.ts. Omettere l'estensione in un file ES module dà l'errore TS2835.
Meglio export default o export con nome in TypeScript?
Funzionano entrambi. Molti team preferiscono gli export con nome: il nome è lo stesso in ogni file che lo importa, gli editor li importano in automatico in modo affidabile, e rinominare diventa un refactoring invece di una ricerca. Un export di default lascia a ogni file che lo importa la scelta del nome.
L'opzione paths di tsconfig cambia l'import nell'output?
No. paths dice solo al controllo dei tipi dove trovare un modulo. Il JavaScript generato mantiene "@lib/util" così com'è scritto, quindi a runtime deve risolverlo un bundler, oppure il campo imports di Node nel package.json.
Come risolvo "Cannot find module" in TypeScript?
L'errore TS2307 significa che il percorso non porta a un file che TypeScript riesce a trovare, oppure che un pacchetto non include tipi. Controlla il percorso relativo e l'estensione, installa il pacchetto @types/... se il pacchetto non ha tipi integrati, oppure scrivi un piccolo file di dichiarazione per lui.