require(x) vs import x: почему это вечная головная боль TypeScript-разработчика
Если вы когда-нибудь писали на TypeScript и видели ошибку вроде Cannot use import statement outside a module или require is not defined — вы не одиноки. На Stack Overflow вопрос "The difference between 'require(x)' and 'import x'" собрал десятки тысяч просмотров. И это не случайно: путаница между двумя системами модулей — одна из самых частых ловушек, в которую попадают даже опытные разработчики.
В этом туториале мы не просто перечислим различия. Мы пройдём путь от полного непонимания до уверенного выбора правильного синтаксиса. Вы узнаете, как TypeScript компилирует ваш код, почему require до сих пор жив и когда стоит использовать import.
1. Исторический контекст: откуда взялись две системы
Чтобы понять разницу, нужно заглянуть в историю JavaScript. Изначально в Node.js не было встроенной модульной системы. Разработчики придумали CommonJS — синхронный способ подключать модули через require(). Этот подход работает серверно, но для браузеров не годился — там нужна асинхронная загрузка.
Позже появился стандарт ES Modules (ESM) — официальный модульный синтаксис ECMAScript. TypeScript, как надстройка над JS, поддерживает оба варианта. Но именно здесь и начинается путаница: вы пишете import, а компилятор превращает его в require — или наоборот.
2. Синтаксис: главные отличия
Давайте разложим по полочкам, как выглядят оба подхода.
CommonJS (require)
// Импорт целого модуля
const fs = require('fs');
// Импорт части модуля (деструктуризация)
const { readFile, writeFile } = require('fs');
// Экспорт
module.exports = { myFunction, myVariable };
exports.myFunction = () => { ... };
ES Modules (import)
// Импорт целого модуля
import fs from 'fs';
// Импорт именованных экспортов
import { readFile, writeFile } from 'fs';
// Импорт всего как объект
import * as fs from 'fs';
// Экспорт
export const myVariable = 42;
export function myFunction() { ... }
export default class MyClass { }
На первый взгляд разница только в синтаксисе. Но копнём глубже.
3. Статический vs динамический импорт
Это ключевое различие, которое влияет на то, как работает ваш код.
require— динамический. Вы можете вызвать его в любом месте, внутри условия или функции. Модуль загружается в момент вызова.import— статический. Импорты разрешаются на этапе компиляции, до выполнения кода. Они всегда должны быть на верхнем уровне файла.
Пример динамического require:
let myModule;
if (process.env.NODE_ENV === 'production') {
myModule = require('./production-config');
} else {
myModule = require('./dev-config');
}
Такой фокус с import не пройдёт — будет синтаксическая ошибка. Но для этого есть динамический import() (возвращает Promise):
if (condition) {
const module = await import('./some-module');
module.doSomething();
}
4. Как TypeScript компилирует модули
Всё зависит от настройки module в tsconfig.json. Вот основные варианты:
"module": "commonjs"— вашimportпревратится вrequire. Код будет работать в Node.js без дополнительных настроек."module": "esnext"—importостанется как есть. Подходит для современных браузеров или сборщиков (Webpack, Vite)."module": "node16"или"nodenext"— TypeScript сам определяет, какую систему использовать, исходя из расширения файла иpackage.json.
Пример: вы пишете import x from 'x' в TypeScript, а после компиляции получаете const x = require('x') — если стоит CommonJS.
5. Практический пример: когда require всё ещё нужен
Несмотря на то, что import — современный стандарт, require остаётся полезным в некоторых сценариях:
- Условная загрузка модулей (как в примере выше)
- Подключение JSON-файлов (в CommonJS — нативно, в ESM — нужен
assert { type: 'json' }) - Старые проекты, которые ещё не перешли на ES Modules
- Быстрое прототипирование без настройки
package.json
Пример с JSON:
// CommonJS
const config = require('./config.json');
// ES Modules (TypeScript)
import config from './config.json' assert { type: 'json' };
6. Как правильно настроить TypeScript для ES Modules
Если вы хотите использовать современный import, следуйте этим шагам:
- В
package.jsonдобавьте"type": "module" - В
tsconfig.jsonустановите"module": "esnext"или"node16" - Убедитесь, что в импортах указываете расширение файла (например,
import { foo } from './bar.js')
Пример рабочего tsconfig.json для ESM:
{
"compilerOptions": {
"target": "ES2022",
"module": "node16",
"moduleResolution": "node16",
"esModuleInterop": true,
"strict": true
}
}
7. Подводные камни: что часто ломается
Даже если вы всё настроили правильно, есть типичные ошибки:
- Ошибка:
require is not defined— вы используетеrequireв среде, где включены только ES Modules (например, в браузере). Решение: используйтеimportили динамическийimport(). - Ошибка:
Cannot use import statement outside a module— вы используетеimport, но Node.js думает, что файл — CommonJS. Решение: добавьте"type": "module"вpackage.jsonили переименуйте файл в.mjs. - Путаница с
export default— в CommonJSexport defaultпревращается вmodule.exports.default. Чтобы импортировать такой модуль черезimport x from 'x', включите"esModuleInterop": true