Перейти к содержимому
Теория · Проект на TypeScript · урок 1 из 10

Модули

Чему вы научитесь

Чему вы научитесь:

  • делить программу на модули через export и import и отличать модуль от скрипта;
  • объяснять, что остаётся в JavaScript от import type и от обычного импорта;
  • настраивать компилятор для всего проекта через файл tsconfig.json;
  • подключать типы JavaScript-библиотек: файлы .d.ts и пакеты @types.
Дом из двух комнат-модулей с дверью export и import и доской правил tsconfig на стене

Представьте дом. Пока в нём одна комната, все вещи лежат на виду, и любую можно взять. Когда вещей становится много, дом делят на комнаты. На кухне своя посуда, в кабинете свои книги, и из кухни книг не видно. Если книга нужна на кухне, её выносят к двери кабинета, и кухня забирает её через эту дверь.

Программа растёт так же. Сначала весь код помещается в один файл, потом его делят на несколько. В TypeScript, как и в современном JavaScript, комнаты — это файлы-модули, а двери — слова export и import.

Но в доме есть и общий коридор, который видят все. Посмотрим, что случится с вещью, оставленной там. Вот файл hello.ts из двух строк:

TypeScript
const name = "Аня"; // ✗
console.log(name);
TEXT
error TS2451: Cannot redeclare block-scoped variable 'name'.

Давайте разберём, что сказал tsc:

  1. Сообщение означает «нельзя второй раз объявить переменную name», хотя в файле она объявлена один раз.
  2. Это же сообщение tsc печатает и для файла lib.dom.d.ts. В нём tsc описывает всё, что есть в браузере, и там уже объявлено имя name — имя окна браузера.
  3. Значит, наша name попала туда же, где лежит браузерная, — в общий коридор.

Скрипт и модуль

TypeScript делит файлы на два вида по одному признаку: есть ли в файле import или export.

Скрипт

Файл без import и export — это скрипт. Его имена верхнего уровня, то есть объявленные вне функций и блоков, глобальные. Они общие для всех скриптов программы, и среди них уже есть имена самого браузера.

По умолчанию tsc считает, что программа может работать в браузере, и знает его глобальные имена: name, status, length, top и другие. Поэтому const name в скрипте сталкивается с браузерным name.

В картинке с домом скрипт оставляет свои вещи в общем коридоре. Их видят все жильцы, и двух вещей с одной биркой там быть не может.

Модуль

Файл, в котором есть хотя бы один import или export, — это модуль. Его имена верхнего уровня принадлежат только ему, а снаружи видно лишь то, что он экспортирует. Переменная name внутри модуля не мешает браузерной, как книга в кабинете не мешает книге в коридоре.

export и import

Разделим программу на два файла. В format.ts лежит функция, которая показывает цену с двумя знаками после точки:

TypeScript
export function formatPrice(price: number): string {
    return price.toFixed(2);
}

function round(price: number): number {
    return Math.round(price);
}

Что здесь важно:

  • export перед function выставляет функцию к двери: её смогут взять другие файлы.
  • У round слова export нет, поэтому эта функция видна только внутри format.ts.
  • Из-за export файл стал модулем, и его имена не попадают в общий коридор.

Файл main.ts забирает функцию строкой import { formatPrice } from "./format.js";. Разберём её по частям:

  1. import значит «взять из другого модуля».
  2. В фигурных скобках — нужные имена. Их может быть несколько, через запятую.
  3. После from — путь к модулю, а ./ значит «в той же папке».
  4. В пути стоит .js, а не .ts: так пишут в TypeScript, и tsc сам найдёт рядом format.ts.

После этой строки main.ts вызывает formatPrice как свою функцию. Пусть вторая строка файла — console.log(formatPrice(100));. Скомпилируем и запустим:

TEXT
$ tsc main.ts
$ node main.js
100.00

tsc сам идёт по импортам. Увидев import из ./format.js, он проверяет и format.ts, поэтому рядом появляются оба файла: main.js и format.js.

Типы экспортируют так же, как функции: export interface User { … } или export type Id = number;. Берут их той же строкой import.

Здесь аналогия с дверью перестаёт работать. Книги, вынесенной из кабинета, в кабинете больше нет. Экспортированная функция остаётся в своём модуле: main.ts получает доступ к той же самой функции, а не уносит её.

export {}

Вернёмся к hello.ts. Чтобы он стал модулем, хватит одной строки:

TypeScript
export {};

const name = "Аня";
console.log(name); // Аня

export {} ничего не экспортирует: фигурные скобки пустые. Но слово export в файле есть, значит, это модуль, и name в нём своя, а не глобальная.

В настоящем проекте почти в каждом файле есть import или export, и эта строка не нужна. Она выручает в маленьком файле для опытов, где их нет.

На собеседовании иногда спрашивают, почему переменная из одного файла конфликтует с переменной из другого файла или из браузера. Ответ: файл без import и export — скрипт, и его имена глобальные. Строка export {} делает его модулем.

Частая ошибка

Импортировали функцию, которую модуль не экспортирует. В format.ts у round нет export, а main.ts пытается её взять:

TypeScript
import { round } from "./format.js"; // ✗

console.log(round(99.6));
TEXT
error TS2459: Module '"./format.js"' declares 'round' locally, but it is not exported.

tsc видит функцию round в format.ts, но она объявлена только для своего модуля: двери к ней нет. Добавьте export перед function round, и импорт заработает.

Главное

  • Файл с import или export — модуль: его имена свои, а снаружи видно только то, что он экспортирует.
  • Файл без них — скрипт: его имена глобальные и сталкиваются с именами браузера, как name с ошибкой TS2451.
  • export выставляет функцию или тип наружу, а import { … } from "./file.js" забирает их в другом файле.
  • Строка export {} превращает скрипт в модуль, ничего не экспортируя.