Модуль в TypeScript это файл, в котором есть хотя бы один import или export на верхнем уровне. Всё, что в нём объявлено, приватно для файла, пока вы это не экспортируете через export, а другие файлы импортируют через import то, что им нужно. Синтаксис это синтаксис ES-модулей JavaScript плюс несколько форм только для типов.
Именованный экспорт и экспорт по умолчанию
В проекте много файлов, и импортирующая сторона выглядит так. Экспорт по умолчанию импортируется без фигурных скобок и может получить любое имя; именованные экспорты пишутся в скобках и сохраняют свои имена, если не переименовать их через 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);
| Форма | Что импортирует |
|---|---|
import { a, b } from "./m.js" | именованные экспорты a и b |
import x from "./m.js" | экспорт по умолчанию под именем x |
import * as m from "./m.js" | объект пространства имён со всеми экспортами |
import { a as b } from "./m.js" | a, переименованный в этом файле в b |
import type { T } from "./m.js" | только типы, удаляется из результата |
import "./setup.js" | выполняет файл ради побочных эффектов |
export { a } from "./m.js" | реэкспортирует a, не импортируя его |
export * from "./m.js" | реэкспортирует все именованные экспорты |
Реэкспорт позволяет одному файлу (часто index.ts) собрать публичный API папки. Поведение модулей в обычном JavaScript, например живые привязки и кэширование модулей, описано на странице ES-модули.
import type и export type
Во время выполнения типов не существует, поэтому импорту, который используется только как тип, нечего загружать. import type говорит об этом явно, и инструкция исчезает из JavaScript на выходе. Модификатор type работает и для отдельного имени внутри обычного импорта.
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"];
Без этого ключевого слова TypeScript всё равно удаляет имена, про которые видит, что это только типы. Две настройки делают ключевое слово обязательным:
verbatimModuleSyntax: trueоставляет каждый импорт, не помеченныйtype, поэтому непомеченный импорт типа даёт ошибку TS1484:'Point' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.- Запуск файлов
.tsнапрямую в Node (удаление типов) убирает аннотации типов, но не заглядывает в другие файлы. Непомеченныйimport { Point }остаётся в коде, и Node падает во время выполнения сSyntaxError: The requested module './math.ts' does not provide an export named 'Point'.
Слово type у каждого импорта только типов работает в обоих случаях, так что к нему стоит привыкнуть.
Cannot find module
Когда путь в импорте не ведёт к файлу или пакету с типами, компилятор останавливается с ошибкой TS2307. Запустите этот пример, чтобы её увидеть:
Вывод: index.ts(2,29): error TS2307: Cannot find module './utils.js' or its corresponding type declarations. Обычные причины:
- Опечатка в относительном пути или пропущенное
./(без него имя ищется вnode_modules). - Пакет на JavaScript без встроенных типов: установите
@types/{package}, если такой есть, или напишите для него файл объявлений. - Подпуть, которого пакет не перечисляет в поле
exportsсвоегоpackage.json. При разрешенииnode16,nodenextиbundlerимпортировать можно только перечисленные точки входа.
ES-модули или CommonJS на выходе
Вы всегда пишете import и export. Какой JavaScript получится, решает опция module в tsconfig.json.
// 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));
С node16, node18, node20 или nodenext TypeScript следует собственным правилам Node для каждого файла:
| Файл | Формат на выходе |
|---|---|
.ts в пакете с "type": "module" | ES-модуль |
.ts в пакете без него | CommonJS |
.mts | всегда ES-модуль, генерируется как .mjs |
.cts | всегда CommonJS, генерируется как .cjs |
С module: "esnext" или "preserve" результат сохраняет import/export, чего и ждут бандлеры вроде Vite и esbuild. Различия двух систем модулей во время выполнения описаны на странице CommonJS и ESM.
Расширения файлов в импортах
При module: node16 или nodenext файл ES-модуля должен называть файл, который Node действительно загрузит, а это скомпилированный .js. Для проверки типов TypeScript сопоставляет ./math.js обратно с 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
Текст ошибки: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './math.js'? Файлы CommonJS при тех же настройках могут не указывать расширение, потому что require сам перебирает расширения.
Две другие конфигурации меняют это правило:
moduleResolution: "bundler"(сmodule, равнымesnext,preserveилиcommonjs) принимает./mathбез расширения, потому что его разрешает бандлер.rewriteRelativeImportExtensions: trueпозволяет писать./math.ts, тот же путь, который работает, когда Node запускает файл.tsнапрямую, и переписывает его в./math.jsна выходе.
Разрешение модулей и paths
Для голого имени вроде "zod" TypeScript ищет в node_modules, читает package.json пакета (его поля exports и types) и в крайнем случае обращается к node_modules/@types/zod. Относительные имена (./, ../) разрешаются от импортирующего файла. Импортировать можно и файл .json; нужные для этого настройки описаны на странице JSON.
paths в tsconfig.json добавляет псевдонимы для ваших собственных папок:
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@lib/*": ["./src/lib/*"]
}
}
}
paths влияет только на проверку типов. В сгенерированном файле по-прежнему написано import { v } from "@lib/util", поэтому во время выполнения его должен разрешить кто-то другой: бандлер с тем же псевдонимом или поле imports в package.json для Node (псевдонимы, начинающиеся с #, например "#lib/*": "./dist/lib/*"), которое работает без дополнительных инструментов. В TypeScript 7 baseUrl больше нет (ошибка TS5102); записывайте элементы paths относительно tsconfig.json с ведущим ./.
Часто задаваемые вопросы
Чем import отличается от import type в TypeScript?
import type { User } from "./user.js" может импортировать только типы, и вся инструкция удаляется из JavaScript на выходе. Обычный import может импортировать и значения, и типы; TypeScript отбрасывает имена, которые используются только как типы, но при verbatimModuleSyntax требует помечать их type, чтобы результат совпадал с тем, что вы написали.
Почему TypeScript требует .js в путях импорта?
При module: node16 или nodenext файл ES-модуля должен импортировать по реальному имени файла, который Node загрузит во время выполнения, а это скомпилированный .js. При проверке типов TypeScript сопоставляет ./math.js с math.ts. Пропуск расширения в файле ES-модуля даёт ошибку TS2835.
Что использовать в TypeScript: export default или именованные экспорты?
Работает и то, и другое. Многие команды предпочитают именованные экспорты: имя одинаково в каждом импортирующем файле, редакторы надёжно добавляют их автоимпортом, а переименование становится рефакторингом, а не поиском по тексту. Экспорт по умолчанию позволяет каждому импортирующему файлу выбрать своё имя.
Меняет ли опция paths в tsconfig импорт в результате компиляции?
Нет. paths только сообщает проверке типов, где найти модуль. В сгенерированном JavaScript "@lib/util" остаётся как написано, поэтому во время выполнения его должен разрешить бандлер или поле imports в package.json для Node.
Как исправить «Cannot find module» в TypeScript?
Ошибка TS2307 означает, что путь не ведёт к файлу, который TypeScript может найти, или что пакет не поставляется с типами. Проверьте относительный путь и расширение, установите пакет @types/..., если встроенных типов нет, или напишите для него небольшой файл объявлений.