Пошук уроків, статей та іншого контенту
Налаштуєте типи модулів, module, moduleResolution і зрозумієте, як TypeScript знаходить імпорти та залежності.
Модуль — це файл, який має хоча б один import або export. Модулі допомагають:
розділяти програму на файли;
приховувати внутрішню реалізацію;
явно описувати залежності;
уникати конфліктів між глобальними змінними.
// math.ts
export function add(a: number, b: number): number {
return a + b;
}// index.ts
import { add } from "./math.js";
const result = add(2, 3);
console.log(result);У TypeScript імпорти та експорти спочатку перевіряються компілятором, а потім перетворюються або залишаються у форматі, який очікує середовище виконання.
moduleОпція module визначає, у який формат TypeScript перетворює модулі та як він інтерпретує імпорти у файлах.
Наприклад:
{
"compilerOptions": {
"module": "CommonJS"
}
}Файл:
import { add } from "./math";
console.log(add(2, 3));може бути перетворений приблизно на CommonJS-код:
"use strict";
const math_1 = require("./math");
console.log((0, math_1.add)(2, 3));moduleCommonJSВикористовує require та exports.
{
"compilerOptions": {
"module": "CommonJS"
}
}Цей формат часто зустрічається у старих Node.js-проєктах. Для нових Node.js-проєктів зазвичай краще використовувати NodeNext.
ESNextЗберігає ES-модулі у сучасному вигляді:
import { add } from "./math.js";
export { add };Після компіляції імпорти та експорти залишаються в коді. Це зручно, якщо подальшу обробку виконує bundler або середовище, яке підтримує ES-модулі.
ES2020, ES2022Ці значення визначають версію JavaScript, під яку генерується код ES-модулів. Вони можуть бути потрібні, коли важливо контролювати рівень підтримки можливостей JavaScript.
Node16 та NodeNextЦі значення призначені для Node.js з підтримкою ESM і CommonJS. TypeScript аналізує:
package.json;
поле "type";
розширення файлів;
правила Node.js для імпортів;
поле "exports" у пакетах.
Зазвичай module і moduleResolution для Node.js задають узгоджено:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}PreserveЗберігає форму кожного імпорту та експорту такою, якою вона була у вихідному файлі. Це корисно в сучасних проєктах, де різні модулі можуть оброблятися bundler-ом.
moduleResolutionmoduleResolution визначає, як TypeScript шукає файл, на який посилається імпорт.
Наприклад:
import { add } from "./math.js";TypeScript має визначити:
який саме файл відповідає ./math.js;
чи існує він;
які типи експортує цей файл;
чи доступний імпортований пакет.
module визначає формат модулів, а moduleResolution — алгоритм пошуку модулів.
NodeNextNodeNext моделює сучасні правила Node.js. Вона використовується разом із:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}Вибір між ESM та CommonJS залежить від конфігурації проєкту.
Наприклад, якщо package.json містить:
{
"type": "module"
}то .js-файли трактуються як ES-модулі. TypeScript-файли .ts у такому проєкті також перевіряються з урахуванням ESM-правил.
Якщо "type": "module" відсутнє, Node.js за замовчуванням трактує .js-файли як CommonJS.
Node16Node16 працює за правилами Node.js 16. Вона подібна до NodeNext, але прив'язана до стабільнішої моделі поведінки Node.js 16.
Для більшості сучасних Node.js-проєктів можна використовувати NodeNext, якщо немає потреби зафіксувати поведінку на рівні Node.js 16.
BundlerBundler призначена для проєктів, які збираються bundler-ом. Вона підтримує сучасні поля пакетів, зокрема "exports" та "imports", але допускає імпорти без розширень файлів.
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Bundler"
}
}Це типовий варіант для застосунків, які збираються сучасним bundler-ом.
Node10Node10 — старий алгоритм Node.js, раніше відомий як node. Він використовує традиційний пошук:
файл із вказаним ім'ям;
файл із розширенням .ts, .tsx або .d.ts;
index.ts у каталозі;
package.json та його поле "main".
Цей режим потрібен переважно для старих проєктів.
ClassicClassic — застаріла стратегія TypeScript, яка не моделює правила Node.js. Її не слід використовувати для нових проєктів.
Відносний імпорт починається з:
./ — поточний каталог;
../ — батьківський каталог.
Структура:
src/
index.ts
math.tsІмпорт:
import { add } from "./math.js";У режимі NodeNext розширення .js у вихідному імпорті є важливим. TypeScript розуміє, що під час компіляції цьому імпорту відповідає файл math.ts, а після компіляції — math.js.
Конфігурація:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"strict": true
},
"include": ["src"]
}Файл package.json:
{
"type": "module"
}Файли:
// src/math.ts
export function add(a: number, b: number): number {
return a + b;
}// src/index.ts
import { add } from "./math.js";
console.log(add(2, 3));Після компіляції:
dist/
index.js
math.jsdist/index.js міститиме:
import { add } from "./math.js";
console.log(add(2, 3));Його можна запустити Node.js:
tsc
node dist/index.jsЯкщо в ESM-проєкті написати так:
import { add } from "./math";Node.js не завжди зможе знайти модуль після компіляції, тому TypeScript у режимі NodeNext повідомить про проблему.
Пакетний імпорт не починається з ./ або ../:
import express from "express";
import { readFile } from "node:fs/promises";TypeScript шукає такі модулі серед залежностей проєкту та стандартних декларацій типів.
Типова структура:
project/
node_modules/
some-package/
package.json
dist/
src/
index.ts
package.json
tsconfig.jsonПід час пошуку TypeScript перевіряє пакет за правилами обраного moduleResolution. Для сучасних пакетів важливими можуть бути поля exports та types у їхньому package.json.
Приклад:
{
"name": "some-package",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}У цьому випадку TypeScript використовує:
./dist/index.d.ts для перевірки типів;
./dist/index.js як фактичний ESM-модуль для виконання.
main, module, types та exportsПакет може містити кілька полів, що описують його точки входу:
main — традиційна точка входу для Node.js та CommonJS;
module — неформальне поле, яке деякі bundler-и використовують для ESM;
types або typings — файл декларацій TypeScript;
exports — сучасний механізм опису дозволених точок входу.
Приклад:
{
"name": "calculator",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
}
}У сучасному проєкті TypeScript має використовувати стратегію, яка розуміє exports, наприклад NodeNext або Bundler.
baseUrl та pathsTypeScript дозволяє налаштувати псевдоніми для імпортів:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}Тоді TypeScript може перевіряти такий імпорт:
import { formatDate } from "@/utils/formatDate";Однак важливо: paths змінює лише те, як TypeScript знаходить модулі під час перевірки. Ця опція не переписує імпорт у згенерованому JavaScript.
Тому середовище виконання або bundler також має знати, що означає @/. Самого paths недостатньо для запуску звичайного JavaScript через Node.js.
Для невеликого Node.js-проєкту без bundler-а краще використовувати відносні імпорти або окремо налаштувати підтримку псевдонімів у середовищі виконання.
У проєкті на Node.js потрібно узгодити три речі:
формат модулів у package.json;
значення module;
значення moduleResolution.
package.json:
{
"type": "module"
}tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"strict": true
},
"include": ["src"]
}Імпорт:
import { add } from "./math.js";package.json:
{
"type": "commonjs"
}tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "Node10",
"rootDir": "src",
"outDir": "dist",
"strict": true
},
"include": ["src"]
}У такому режимі TypeScript генерує CommonJS-код. Для нового Node.js-проєкту все одно варто оцінити, чи потрібна сумісність саме з CommonJS, чи краще перейти на ESM через NodeNext.
Структура проєкту:
module-demo/
src/
config.ts
index.ts
package.json
tsconfig.jsonpackage.json:
{
"name": "module-demo",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"noEmitOnError": true
},
"include": ["src/**/*.ts"]
}src/config.ts:
export const appName = "Module Demo";
export const port = 3000;src/index.ts:
import { appName, port } from "./config.js";
console.log(`${appName} працює на порту ${port}`);Команди:
npm run build
npm startОчікуваний результат:
Module Demo працює на порту 3000У цьому прикладі:
package.json визначає проєкт як ESM;
module: "NodeNext" визначає правила генерації та аналізу модулів;
moduleResolution: "NodeNext" визначає пошук імпортів за правилами Node.js;
rootDir задає каталог вихідних файлів;
outDir задає каталог згенерованих файлів;
.js в імпорті відповідає майбутньому файлу dist/config.js.
Якщо TypeScript не знаходить імпорт, можна вивести детальну інформацію про алгоритм пошуку:
tsc --traceResolutionЦе показує:
який імпорт аналізується;
які шляхи перевіряються;
які розширення розглядаються;
чи використовується package.json;
чому конкретний файл або пакет не було знайдено.
Також корисно перевірити фактичну конфігурацію після врахування всіх успадкованих файлів:
tsc --showConfigЦе допомагає виявити ситуації, коли значення module або moduleResolution було змінено в іншому tsconfig.json.
package.json та moduleНаприклад, проєкт має:
{
"type": "module"
}але TypeScript налаштований на:
{
"compilerOptions": {
"module": "CommonJS"
}
}У такому разі згенерований код і спосіб його запуску можуть не збігатися.
Для ESM-проєкту на Node.js використовуйте узгоджену конфігурацію:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}Проблемний варіант:
import { add } from "./math";У NodeNext для ESM зазвичай потрібно:
import { add } from "./math.js";paths без налаштування runtimeTypeScript може прийняти:
import { logger } from "@/logger";але Node.js може не зрозуміти псевдонім @. Потрібно або використовувати відносні імпорти, або налаштувати bundler чи середовище виконання відповідно до цієї схеми.
moduleResolutionЯкщо пакет використовує сучасне поле exports, а проєкт налаштований на стару стратегію, TypeScript може не знайти правильну точку входу або декларації типів.
Для сучасного Node.js використовуйте NodeNext, а для bundler-проєкту — Bundler.
Не варто без чіткої причини змішувати:
import та require;
різні типи модулів;
несумісні значення module і moduleResolution.
Спочатку визначте середовище виконання, а потім налаштуйте TypeScript відповідно до його правил.
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}Додайте "type": "module" у package.json і використовуйте .js у відносних імпортах.
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Bundler"
}
}Bundler самостійно обробляє фінальні шляхи імпортів і формат пакування.
{
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node10"
}
}Таку конфігурацію варто залишати для сумісності з наявною кодовою базою, а не використовувати автоматично в нових проєктах.
module визначає формат модулів і спосіб генерації JavaScript.
moduleResolution визначає, як TypeScript знаходить файли та пакети.
Для сучасного Node.js зазвичай використовують NodeNext для обох опцій.
Для bundler-проєктів часто використовують module: "ESNext" і moduleResolution: "Bundler".
У ESM-проєктах Node.js відносні імпорти мають враховувати розширення вихідного JavaScript-файлу.
package.json із полем "type" впливає на інтерпретацію модулів.
Поля exports і types допомагають TypeScript знайти правильний код та декларації типів у пакетах.
paths впливає на перевірку TypeScript, але самостійно не налаштовує запуск згенерованого JavaScript.
Для діагностики пошуку модулів використовуйте tsc --traceResolution.