Menu

Moduli in TypeScript: import, export e import type

Ogni file TypeScript con un import o un export al livello più alto è un modulo. Impara gli export con nome e quelli di default, import type ed export type, come l'opzione module sceglie tra output ES module e CommonJS, e perché node16 e nodenext vogliono le estensioni .js negli import.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

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);
FormaCosa 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: true mantiene ogni import non marcato con type, 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 .ts direttamente in Node (type stripping) rimuove le annotazioni di tipo ma non guarda gli altri file. Un import { Point } non marcato resta nel codice, e Node fallisce a runtime con SyntaxError: 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 in node_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 exports del suo package.json. Con la risoluzione node16, nodenext e bundler, 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:

FileFormato dell'output
.ts in un pacchetto con "type": "module"ES module
.ts in un pacchetto senzaCommonJS
.mtssempre ES module, generato come .mjs
.ctssempre 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" (con module impostato a esnext, preserve o commonjs) accetta ./math senza estensione, perché è il bundler a risolverlo.
  • rewriteRelativeImportExtensions: true ti permette di scrivere ./math.ts, lo stesso percorso che funziona quando Node esegue direttamente il file .ts, e lo riscrive in ./math.js nell'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.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA