TypeScript використовує синтаксис ES-модулів: файл з import чи export на верхньому рівні - модуль зі своєю областю видимості.
// money.ts
export function formatPrice(amount: number): string {
return `${amount.toFixed(2)} грн`;
}
export type Currency = 'UAH' | 'USD';
// app.ts
import { formatPrice, type Currency } from './money.js';
Файл без import/export - скрипт: його оголошення потрапляють у глобальну область. Тому в TypeScript-файлах інколи пишуть порожній export {} - щоб файл став модулем.
Чому .js в імпорті .ts-файлу. TypeScript не переписує шляхи імпортів при компіляції: що написано в коді, те й опиниться в зібраному JavaScript. Після компіляції money.ts стане money.js, і Node.js шукатиме саме ./money.js. Тому з moduleResolution: "nodenext" відносні імпорти пишуть з розширенням виконуваного файлу - .js, а TypeScript розуміє, що йдеться про money.ts.
Що залежить від налаштувань:
moduleResolution: "bundler"(Vite, webpack, esbuild) - розширення можна не писати: збирач сам знайде файл. Найзручніше для фронтенду;moduleResolution: "nodenext"- правила Node.js: розширення обов'язкове для ES-модулів, а тип модуля (ESM чи CommonJS) визначається полем"type"уpackage.jsonабо розширенням.mts/.cts;- запуск
.tsнапряму (стирання типів у Node.js, Bun, Deno) - можна імпортувати з.tsзаallowImportingTsExtensions; для бібліотек, що компілюються,rewriteRelativeImportExtensionsперепише.tsна.jsу результаті.
TypeScript 6/7: старі режими moduleResolution: "node" (node10) і "classic" видалено - лишилися nodenext і bundler. Типове значення module - esnext.
Корисне правило: спосіб резолюції модулів у tsconfig має відповідати тому, хто реально виконує код - збирач, Node.js чи інший рушій. Інакше TypeScript погоджуватиметься з імпортами, які не працюють під час виконання.