require vs import в TypeScript: пошаговый туториал для путающихся

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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, следуйте этим шагам:

  1. В package.json добавьте "type": "module"
  2. В tsconfig.json установите "module": "esnext" или "node16"
  3. Убедитесь, что в импортах указываете расширение файла (например, 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 — в CommonJS export default превращается в module.exports.default. Чтобы импортировать такой модуль через import x from 'x', включите "esModuleInterop": true

Блоги

Рекомендаций по книгам