📘 TypeScript

Модули, пакеты и импорт в TypeScript: подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты строишь огромный дом. Ты можешь попытаться сделать всё сам: замесить бетон, вырезать окна, провести электричество, собрать мебель. Но это займёт годы, и результат, скорее всего, будет ужасным. Гораздо умнее поступить иначе: купить готовые блоки для стен, заказать пластиковые окна у специалистов, нанять электрика и купить мебель в магазине.

Точно так же устроено программирование. Когда ты пишешь программу на TypeScript, ты не обязан создавать всё с нуля. Ты можешь брать готовые «кирпичики» — модули и пакеты — и соединять их вместе. Это экономит время, нервы и делает код понятным.

Давай разберёмся с терминами на примере библиотеки:

  • Модуль — это как одна книга на полке. В ней собраны знания по одной теме: например, «Как работать с датами» или «Как рисовать графики». В программировании модуль — это отдельный файл с кодом, который делает что-то конкретное.
  • Пакет — это целая полка (или даже шкаф) с книгами. Пакет объединяет несколько модулей, которые нужны для решения большой задачи. Например, пакет «Строительство дома» может содержать модули: «Фундамент», «Стены», «Крыша».
  • Импорт — это когда ты идёшь к полке, берёшь нужную книгу и начинаешь её читать. В коде это команда, которая говорит: «Возьми функцию или переменную из другого файла и дай мне её использовать».
💡 Важно: Модули и пакеты — это способ не изобретать велосипед. Если кто-то уже написал отличный код для работы с датами, зачем писать свой? Просто импортируй готовый!

Зачем это нужно? Представь, что твоя программа — это текст на 10 000 строк. Если всё хранить в одном файле, ты быстро запутаешься: где искать ошибку? Как понять, что делает каждая часть? Модули решают эту проблему: ты разбиваешь код на маленькие кусочки, каждый из которых отвечает за свою задачу. Это называется модульность.

В TypeScript (и в современном JavaScript) модули — это основа. Без них невозможно представить ни один серьёзный проект. Они помогают:

  • Делить код на логические части.
  • Переиспользовать код в разных проектах.
  • Скрывать внутренние детали (не всё, что есть в модуле, обязательно показывать наружу).
  • Работать в команде: каждый пишет свой модуль, а потом все соединяют.

В этой статье мы изучим, как работают модули и пакеты в TypeScript, как их импортировать и экспортировать, и как не наделать ошибок. Поехали!

Теория: как это работает изнутри

Чтобы понять модули, нужно заглянуть «под капот» TypeScript. Когда ты пишешь код, ты используешь систему модулей. Это набор правил, которые определяют, как файлы общаются друг с другом.

Что такое модуль в TypeScript?

Любой файл с расширением .ts или .tsx может стать модулем. Но есть условие: если в файле есть хотя бы одна команда import или export, он считается модулем. Если таких команд нет — это просто скрипт, и его переменные «видны» всем (что плохо, потому что можно случайно перезаписать чужие данные).

Модуль — это изолированное пространство. Всё, что ты объявляешь внутри модуля (переменные, функции, классы), по умолчанию видно только внутри этого модуля. Чтобы поделиться чем-то с другими файлами, нужно явно сказать: «Это я разрешаю использовать снаружи». Для этого используется команда export.

Как работает импорт?

Когда ты пишешь import { something } from './file', TypeScript (а потом и JavaScript) делает следующее:

  1. Находит файл file.ts (или file.js, если это скомпилированный код).
  2. Загружает его содержимое.
  3. Ищет в нём то, что помечено как export и совпадает по имени.
  4. Делает это доступным в текущем файле.

Важно понимать: модули загружаются только один раз. Если два файла импортируют один и тот же модуль, он не будет загружен дважды. JavaScript умный: он кеширует модули. Это экономит память и время.

Пути и разрешение модулей

Когда ты указываешь путь в import, TypeScript ищет файл по определённым правилам. Например:

  • './module' — относительный путь. Ищет файл module.ts в той же папке.
  • '../utils/helper' — подняться на папку выше, потом зайти в utils и найти helper.ts.
  • 'lodash' — без точки в начале. Это означает, что нужно искать пакет в папке node_modules. Так подключаются библиотеки, установленные через npm.

TypeScript также поддерживает разрешение модулей — он может искать файлы с разными расширениями: .ts, .tsx, .d.ts, .js. Это настраивается в файле tsconfig.json.

Компиляция: как TypeScript превращается в JavaScript

TypeScript — это надстройка над JavaScript. Браузеры и Node.js не понимают TypeScript напрямую. Поэтому код нужно скомпилировать (перевести) в JavaScript. При компиляции модули TypeScript превращаются в модули JavaScript. В зависимости от настроек, это могут быть:

  • CommonJS (require/module.exports) — используется в Node.js.
  • ES Modules (import/export) — современный стандарт для браузеров и новых версий Node.js.
  • AMD, UMD, System — более старые форматы, сейчас редко используются.

В этой статье мы будем говорить о современном синтаксисе import/export, который поддерживается и TypeScript, и современным JavaScript.

⚠️ Важно: Если ты пишешь код для браузера, убедись, что твой сборщик (Webpack, Vite, Parcel) настроен на работу с ES модулями. Если для Node.js — проверь, что в package.json есть "type": "module".

Синтаксис в TypeScript: все формы и варианты

TypeScript предлагает несколько способов экспортировать и импортировать код. Давай разберём каждый из них с примерами.

Экспорт: как отдать код наружу

Есть два основных вида экспорта: именованный и экспорт по умолчанию.

1. Именованный экспорт (named export)

Ты можешь экспортировать несколько сущностей из одного модуля. Каждая из них имеет своё имя.

// файл: mathUtils.ts

// Экспортируем функцию
export function add(a: number, b: number): number {
    return a + b;
}

// Экспортируем переменную
export const PI = 3.14159;

// Экспортируем класс
export class Calculator {
    multiply(a: number, b: number): number {
        return a * b;
    }
}

2. Экспорт по умолчанию (default export)

Из модуля можно экспортировать одну главную сущность. Это удобно, когда модуль делает что-то одно.

// файл: logger.ts

// Экспорт по умолчанию — функция
export default function log(message: string): void {
    console.log(`[LOG]: ${message}`);
}

Можно также экспортировать по умолчанию класс или объект:

// файл: config.ts

// Экспорт по умолчанию — объект
const config = {
    apiUrl: 'https://api.example.com',
    timeout: 5000
};

export default config;

3. Комбинированный экспорт

В одном модуле можно одновременно использовать именованный экспорт и экспорт по умолчанию. Но так делать не рекомендуется, чтобы не путать читателей кода.

// файл: utils.ts

export function helper1() { /* ... */ }
export function helper2() { /* ... */ }
export default function mainHelper() { /* ... */ }

4. Реэкспорт (re-export)

Иногда удобно собрать все экспорты из разных модулей в одном месте и экспортировать их снова. Это называется «баррель» (barrel).

// файл: index.ts

// Реэкспортируем всё из mathUtils
export * from './mathUtils';

// Реэкспортируем конкретные сущности
export { log } from './logger';

// Реэкспортируем по умолчанию под другим именем
export { default as appConfig } from './config';

Импорт: как взять код из другого модуля

Теперь посмотрим, как импортировать то, что мы экспортировали.

1. Импорт именованных экспортов

// файл: main.ts

// Импортируем конкретные функции и переменные
import { add, PI, Calculator } from './mathUtils';

// Теперь можно использовать
const sum = add(2, 3); // 5
const calc = new Calculator();
const result = calc.multiply(4, 5); // 20
console.log(PI); // 3.14159

2. Импорт с переименованием

Если имя экспортированной сущности конфликтует с чем-то в твоём файле, можно дать ей другое имя.

// Импортируем add, но называем её sumFunction
import { add as sumFunction } from './mathUtils';

console.log(sumFunction(10, 20)); // 30

3. Импорт всего модуля как объекта

Можно импортировать всё, что экспортирует модуль, в виде одного объекта.

// Импортируем всё из mathUtils в объект MathUtils
import * as MathUtils from './mathUtils';

console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(1, 2)); // 3
const calc = new MathUtils.Calculator();

4. Импорт экспорта по умолчанию

Для импорта по умолчанию можно дать любое имя, потому что экспорт по умолчанию — это «главный» экспорт модуля.

// Импортируем функцию log из logger.ts
import log from './logger';

// Теперь можно использовать
log('Программа запущена');

5. Импорт и именованного, и по умолчанию

// Импортируем и default, и именованные экспорты
import log, { helper1, helper2 } from './utils';

log('Старт');
helper1();
helper2();

6. Импорт для побочных эффектов

Иногда модуль нужно просто выполнить, чтобы он что-то сделал (например, установил глобальные стили или полифиллы). В этом случае импортируют без фигурных скобок.

// Просто выполняем код из модуля
import './polyfills';
import './styles.css';

7. Импорт типов (TypeScript-specific)

TypeScript позволяет импортировать только типы, чтобы не включать их в скомпилированный JavaScript. Это оптимизация.

// Импортируем только тип
import type { User } from './models';

// Или импортируем и тип, и значение
import { type User, createUser } from './models';

Справочная таблица: формы экспорта и импорта

Действие Синтаксис Пример
Именованный экспорт export function/const/class export const PI = 3.14;
Экспорт по умолчанию export default export default function() {}
Импорт именованных import { name } from 'path' import { add } from './math'
Импорт с переименованием import { name as alias } import { add as sum }
Импорт всего модуля import * as obj from 'path' import * as Utils from './utils'
Импорт по умолчанию import name from 'path' import log from './logger'
Импорт типов import type { Type } from 'path' import type { User } from './models'
Реэкспорт export * from 'path' export * from './math'

Примеры кода — от простого к сложному

Давай посмотрим на реальные примеры, как модули работают в проекте. Мы создадим маленькое приложение для управления списком задач (todo list).

Пример 1: Простой модуль с одной функцией

// Файл: greetings.ts
// Простой модуль, который экспортирует одну функцию

export function sayHello(name: string): string {
    return `Привет, ${name}!`;
}
// Файл: app.ts
// Импортируем функцию и используем её

import { sayHello } from './greetings';

const message = sayHello('Анна');
console.log(message); // Выведет: Привет, Анна!

Объяснение: В первом файле мы создали функцию и пометили её словом export. Во втором файле мы импортировали её с помощью фигурных скобок и использовали. Всё просто!

Пример 2: Модуль с несколькими экспортами

// Файл: todoTypes.ts
// Определяем типы и интерфейсы для нашего приложения

export interface Task {
    id: number;
    title: string;
    completed: boolean;
}

export type TaskStatus = 'active' | 'completed' | 'all';

export const DEFAULT_STATUS: TaskStatus = 'all';
// Файл: todoService.ts
// Сервис для работы с задачами

import { Task, TaskStatus, DEFAULT_STATUS } from './todoTypes';

// Хранилище задач (в реальном проекте здесь была бы база данных)
let tasks: Task[] = [];
let nextId = 1;

export function addTask(title: string): Task {
    const newTask: Task = {
        id: nextId++,
        title,
        completed: false
    };
    tasks.push(newTask);
    return newTask;
}

export function getTasks(status: TaskStatus = DEFAULT_STATUS): Task[] {
    if (status === 'all') {
        return tasks;
    }
    return tasks.filter(task => 
        status === 'completed' ? task.completed : !task.completed
    );
}

export function toggleTask(id: number): boolean {
    const task = tasks.find(t => t.id === id);
    if (task) {
        task.completed = !task.completed;
        return true;
    }
    return false;
}

Объяснение: Здесь мы разделили типы и логику. todoTypes.ts содержит только описания данных, а todoService.ts — функции для работы с ними. Обрати внимание, что переменная tasks не экспортируется — она скрыта внутри модуля. Это называется инкапсуляция.

Пример 3: Экспорт по умолчанию для главного класса

// Файл: taskRenderer.ts
// Класс для отрисовки задач в HTML (если бы мы работали с браузером)

export default class TaskRenderer {
    private container: HTMLElement;

    constructor(containerId: string) {
        const el = document.getElementById(containerId);
        if (!el) {
            throw new Error(`Элемент с id "${containerId}" не найден`);
        }
        this.container = el;
    }

    render(tasks: Task[]): void {
        this.container.innerHTML = tasks.map(task => `
            
${task.title}
`).join(''); } }
// Файл: main.ts
// Главный файл приложения

import TaskRenderer from './taskRenderer';
import { addTask, getTasks } from './todoService';

// Создаём рендерер
const renderer = new TaskRenderer('app');

// Добавляем несколько задач
addTask('Изучить модули TypeScript');
addTask('Написать статью');
addTask('Сделать домашнее задание');

// Получаем все задачи и отображаем
const allTasks = getTasks();
renderer.render(allTasks);

Объяснение: Класс TaskRenderer — это главная сущность модуля, поэтому мы экспортируем его по умолчанию. В main.ts мы импортируем его без фигурных скобок и даём любое имя (в данном случае TaskRenderer).

Пример 4: Использование пакетов из npm

// Файл: dateHelper.ts
// Используем популярный пакет date-fns для работы с датами

// Сначала нужно установить пакет: npm install date-fns
import { format, addDays, isWeekend } from 'date-fns';

export function formatDate(date: Date): string {
    // Форматируем дату в русском формате
    return format(date, 'dd.MM.yyyy');
}

export function getNextWorkday(date: Date): Date {
    let nextDay = addDays(date, 1);
    // Пропускаем выходные
    while (isWeekend(nextDay)) {
        nextDay = addDays(nextDay, 1);
    }
    return nextDay;
}

Объяснение: Пакет date-fns — это библиотека, установленная через npm. Мы импортируем только те функции, которые нам нужны (format, addDays, isWeekend). Это называется деревом тряски (tree shaking) — в итоговый код попадут только используемые функции, а не вся библиотека.

Пример 5: Реэкспорт через индексный файл

// Файл: models/index.ts
// Собираем все модели в одном месте

export { Task } from './task';
export { User } from './user';
export { Project } from './project';
export type { TaskStatus } from './task';
// Файл: app.ts
// Теперь можно импортировать всё из одной папки

import { Task, User, Project } from './models';

const task: Task = { id: 1, title: 'Купить молоко', completed: false };
const user: User = { id: 1, name: 'Иван' };

Объяснение: Вместо того чтобы запоминать, в каком файле какой тип лежит, мы создаём «баррель» — файл index.ts, который реэкспортирует всё из папки. Теперь можно импортировать всё одной строкой.

Пример 6: Динамический импорт

// Файл: heavyModule.ts
// Предположим, это тяжёлый модуль, который не нужно загружать сразу

export function calculateComplexity(data: number[]): number {
    // Какие-то сложные вычисления
    return data.reduce((acc, val) => acc + Math.sqrt(val), 0);
}
// Файл: app.ts
// Загружаем модуль только когда он понадобится

async function processData(data: number[]) {
    // Динамический импорт — модуль загрузится только здесь
    const heavyModule = await import('./heavyModule');
    
    const result = heavyModule.calculateComplexity(data);
    console.log(`Результат: ${result}`);
}

// Модуль не загружен, пока не вызвана функция
console.log('Приложение запущено');

// Вызываем функцию — модуль загружается
processData([1, 2, 3, 4, 5]);

Объяснение: Динамический импорт (import()) возвращает Promise. Это позволяет загружать модули «лениво» — только когда они действительно нужны. Это ускоряет начальную загрузку приложения.

Пример 7: Импорт типов и интерфейсов

// Файл: apiTypes.ts
// Определяем типы для API

export interface ApiResponse {
    data: T;
    status: number;
    message: string;
}

export type UserRole = 'admin' | 'user' | 'guest';

export interface UserProfile {
    id: number;
    name: string;
    role: UserRole;
}
// Файл: apiClient.ts
// Клиент для работы с API

// Импортируем только типы — они не попадут в скомпилированный код
import type { ApiResponse, UserProfile } from './apiTypes';

export async function fetchUser(id: number): Promise> {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
}

Объяснение: Ключевое слово type в импорте говорит TypeScript: «Мне нужны только типы, не включай их в JavaScript». Это делает итоговый код легче и чище.

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаком с Python, тебе будет проще понять модули в TypeScript. Давай сравним их.

Характеристика TypeScript Python
Импорт модуля import { name } from './module' from module import name
Импорт всего модуля import * as mod from './module' import module
Экспорт по умолчанию export default function() {} Нет прямого аналога (в Python всё — именованный экспорт)
Реэкспорт export * from './module' Нет прямого аналога (можно через __init__.py)
Пакеты Папка с index.ts или package.json Папка с __init__.py
Установка пакетов npm install package pip install package
Динамический импорт import('./module') (асинхронно) importlib.import_module('module')
Импорт типов import type { Type } Нет аналога (типы — в документации)

Основные отличия:

  1. Экспорт по умолчанию: В Python нет понятия «главного» экспорта. В TypeScript это удобно, когда модуль делает что-то одно.
  2. Фигурные скобки: В TypeScript при импорте именованных экспортов нужны фигурные скобки. В Python — нет.
  3. Пути: В TypeScript нужно указывать расширение файла (хотя современные сборщики могут его опускать). В Python расширение .py не указывается.
  4. Типы: TypeScript позволяет импортировать только типы, что оптимизирует код. В Python типы — это просто аннотации, они не влияют на импорт.
  5. Асинхронность: Динамический импорт в TypeScript асинхронный (возвращает Promise). В Python — синхронный.
💡 Совет: Если ты переходишь с Python на TypeScript, запомни главное правило: в TypeScript ты всегда явно указываешь, что экспортируешь. В Python всё, что не начинается с подчёркивания, считается публичным. В TypeScript — наоборот: всё приватное, пока не скажешь export.

Типичные ошибки и подводные камни

Даже опытные разработчики иногда попадают в ловушки при работе с модулями. Вот самые распространённые ошибки.

Ошибка 1: Забыл указать расширение файла

// ❌ Неправильно
import { add } from './mathUtils';

// ✅ Правильно (в большинстве случаев)
import { add } from './mathUtils.ts';
// Или, если настроен сборщик, можно без расширения

Почему это проблема: В старых версиях Node.js и некоторых сборщиках нужно указывать расширение. Лучше всегда указывать .ts или .js (в зависимости от того, что ты компилируешь).

Ошибка 2: Циклические зависимости

// Файл: a.ts
import { b } from './b';
export const a = b + 1;

// Файл: b.ts
import { a } from './a';
export const b = a + 1;

Почему это проблема: Модули пытаются загрузить друг друга, и возникает бесконечный цикл. TypeScript может выдать ошибку или вернуть undefined. Решение: пересмотреть архитектуру и вынести общие зависимости в третий модуль.

Ошибка 3: Путаница между default и named экспортом

// ❌ Неправильно: пытаемся импортировать default как именованный
import { log } from './logger'; // Если log — это export default

// ✅ Правильно:
import log from './logger';

Почему это проблема: Если ты используешь фигурные скобки, TypeScript ищет именованный экспорт. Если его нет — ошибка. Запомни: default экспорт импортируется без фигурных скобок.

Ошибка 4: Импорт несуществующего экспорта

// ❌ Ошибка: функция multiply не экспортируется
import { multiply } from './mathUtils';

// ✅ Нужно проверить, что экспортируется
import { add, PI, Calculator } from './mathUtils';

Почему это проблема: TypeScript выдаст ошибку на этапе компиляции. Но если ты используешь JavaScript без строгой типизации, ошибка проявится только в рантайме.

Ошибка 5: Импорт типов как значений

// ❌ Неправильно: пытаемся использовать тип как значение
import { User } from './models';
const user = new User(); // Ошибка! User — это интерфейс, не класс

// ✅ Правильно: интерфейсы — это только для описания формы объекта
import type { User } from './models';
const user: User = { id: 1, name: 'Анна' };

Поч