Сегодня нас ждет один из тех уроков, который кардинально меняет подход к организации кода. Мы перестаем быть кулинарами, которые готовят все блюда в одной кастрюле и становимся шеф-поварами с правильно организованной кухней, где у каждого ингредиента и инструмента есть свое место.
Мы поговорим о модулях и namespace. Эти концепции позволяют нам дробить наш код на логические, независимые части, хранящиеся в разных файлах. Это не только делает код чище и читабельнее, но и открывает дорогу к работе в команде, повторному использованию кода и его эффективному сопровождению. И самое главное мы научимся импортировать и экспортировать типы, интерфейсы и enum’ы.
Проблема: один огромный файл это хаос
Давайте на мгновение представим, что мы пишем большое веб-приложение. У нас есть десятки сущностей: User, Product, Order, есть множество вспомогательных функций для валидации, форматирования дат, математических расчетов, а также различные константы и настройки.
Если мы будем держать весь этот код в одном файле app.ts, мы очень быстро столкнемся с колоссальными проблемами:
-
Нечитаемость. Найти нужную функцию или тип в файле на тысячи строк, это как искать иголку в стоге сена.
-
Конфликты имен. Мы можем случайно объявить две разные функции с одним и тем же именем
calculateTotal, и компилятор будет очень недоволен. -
Сложность работы в команде. Представьте, что вы и ваш коллега одновременно работаете в одном гигантском файле. Вероятность конфликта при слиянии изменений (merge conflict) стремится к 100%.
-
Сложность повторного использования. Если мы захотим использовать наши прекрасные типы и функции в другом проекте, нам придется вручную копировать их из этого монолитного файла, рискуя утащить с собой ненужный мусор.
Модульная система TypeScript, это элегантное решение всех этих проблем.
Решение: модули
Модуль в TypeScript, это просто файл, который содержит код. Сама по себе идея не нова и реализована в стандарте ECMAScript (ES6 и выше). TypeScript полностью поддерживает этот стандарт.
Ключевая идея модуля проста. То, что явно не помечено как публичное, является приватным. Файл-модуль определяет свою собственную область видимости. Переменные, функции, классы, объявленные в модуле, не видны снаружи, если они не были экспортированы с помощью ключевого слова export. Соответственно, чтобы использовать код из одного модуля в другом, его нужно импортировать с помощью ключевого слова import.
Давайте сразу перейдем к практике и увидим, как это работает с типами.
Экспорт и импорт типов
Одно из greatest advantages TypeScript ,это возможность экспортировать и импортировать типы точно так же, как и любые другие значения (функции, переменные, классы). Для системы типов TypeScript нет разницы, она seamlessly интегрирует типы в модульную систему.
Создаем модуль с типами
Давайте создадим наш первый модуль. Создадим файл types.ts.
// Файл: types.ts // 1. Экспорт типа с помощью ключевого слова 'export' export type UserId = number | string; // 2. Экспорт интерфейса export interface User { id: UserId; name: string; email: string; isActive: boolean; } // 3. Экспорт enum export enum UserRole { Admin = 'ADMIN', Editor = 'EDITOR', Customer = 'CUSTOMER' } // 4. Мы можем объявить тип, который используется только внутри этого модуля. // Он не экспортируется, поэтому снаружи файла types.ts о нем никто не узнает. type InternalLogType = 'debug' | 'error'; // 5. Мы также можем экспортировать функции, использующие наши типы. export function createUser(name: string, email: string): User { return { id: generateUserId(), // предположим, что эта функция есть ниже name, email, isActive: true }; } // 6. Эта функция не экспортируется. Она приватная для этого модуля. function generateUserId(): UserId { return Math.floor(Math.random() * 1000); }
В этом файле мы создали настоящую сокровищницу: мы экспортировали тип UserId, интерфейс User, enum UserRole и функцию createUser. А вот тип InternalLogType и функция generateUserId остаются приватными и не доступны для импорта в других файлах.
Импортируем типы в другой модуль
Теперь давайте создадим главный файл нашего приложения, например, app.ts и импортируем в него все необходимое.
// Файл: app.ts // 1. Именованный импорт. Мы импортируем только то, что нам нужно, явно указав имена. import { User, UserRole, createUser } from './types'; const newUser: User = createUser('Максим', 'max@example.com'); console.log(newUser); // { id: 123, name: "Максим", ... } // 2. Мы можем импортировать все экспортированные сущности под одним именем (namespace). import * as MyTypes from './types'; const adminUser: MyTypes.User = { id: 1, name: 'Администратор', email: 'admin@example.com', isActive: true, }; console.log(adminUser); // 3. Импорт типа с переименованием (алиасом) import { UserId as IdType } from './types'; const someId: IdType = 'abc-123-def'; console.log(someId); function setUserRole(user: User, role: UserRole): void { // ... логика назначения роли console.log(`Пользователю ${user.name} назначена роль: ${role}`); } setUserRole(newUser, UserRole.Admin);
Обратите внимание на пути импорта: from './types'. Указание расширения .ts или .js обычно не требуется. TypeScript и сборщики (как Webpack) сами поймут, какой файл имеется в виду. Путь './types' означает «ищи файл types.ts (или types.js, types/index.ts) в той же папке, где находится текущий файл app.ts«.
Default Export (экспорт по умолчанию)
Помимо именованного экспорта, существует понятие экспорта по умолчанию. В модуле может быть только один такой экспорт. Это часто используется для экспорта главной сущности модуля, например, класса.
Давайте создадим модуль Logger.ts с default export.
// Файл: Logger.ts // 1. Определяем интерфейс для конфигурации (он будет использоваться только здесь) interface LoggerConfig { logLevel: 'info' | 'warn' | 'error'; } // 2. Создаем класс - главную сущность нашего модуля class Logger { constructor(private config: LoggerConfig) {} log(message: string): void { console.log(`[LOG]: ${message}`); } } // 3. Экспортируем класс по умолчанию export default Logger; // 4. Мы можем также делать именованные экспорты рядом с default export const DEFAULT_LOG_LEVEL = 'info';
Теперь импортируем его в app.ts. Синтаксис импорта по умолчанию отличается.
// Файл: app.ts (продолжение) // Импорт default export. Мы можем назвать его как угодно, например, MyLogger. import MyLogger, { DEFAULT_LOG_LEVEL } from './Logger'; const logger = new MyLogger({ logLevel: DEFAULT_LOG_LEVEL }); logger.log('Модуль Logger успешно импортирован!');
Важный момент: хотя типы и можно экспортировать по умолчанию, это считается не самой лучшей практикой. Именованный экспорт типов предпочтительнее, так как он делает импорт более явным и предотвращает путаницу с именами.
Namespace (пространства имен): устаревший, но важный подход
До того как модули в ES6 стали стандартом де-факто, в TypeScript (и JavaScript) существовала проблема организации кода и избежания конфликтов имен. Решением стали пространства имен (namespace, ранее назывались internal modules).
Пространства имен, это способ логической группировки кода внутри одного файла или across multiple files. Они создают свою область видимости.
Синтаксис namespace
Объявляются с помощью ключевого слова namespace.
// Файл: utils/stringHelpers.ts namespace StringHelpers { // Функция экспортируется из namespace и будет доступна снаружи export function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1); } // Эта функция приватная внутри namespace function reverse(str: string): string { return str.split('').reverse().join(''); } } // Чтобы использовать код вне namespace, мы обращаемся к нему через точку. const capitalizedName = StringHelpers.capitalize('максим'); console.log(capitalizedName); // "Максим" // Ошибка! Функция reverse не экспортирована из namespace. // StringHelpers.reverse('hello');
Это похоже на статический класс в других языках.
Проблема: как использовать namespace в разных файлах?
Чтобы использовать один и тот же namespace across multiple files, нам нужно немного магии. Каждый файл, который вносит вклад в namespace, должен объявить его с тем же самым именем и использовать ключевое слово /// <reference path="..." /> для указания на другие файлы, являющиеся зависимостями.
Это устаревший и громоздкий подход, но вы можете встретить его в старых кодовых базах.
-
Создаем файл
Types.ts
// Файл: Types.ts namespace MyApp { export interface User { id: number; name: string; } }
-
Создаем файл
Functions.ts
// Файл: Functions.ts /// <reference path="Types.ts" /> namespace MyApp { export function createUser(name: string): User { // Мы используем User из того же namespace! return { id: 1, name }; } }
-
Создаем главный файл
app.ts
// Файл: app.ts /// <reference path="Types.ts" /> /// <reference path="Functions.ts" /> // Теперь мы можем использовать все, что экспортировано в namespace MyApp const user: MyApp.User = MyApp.createUser('Максим');
Чтобы скомпилировать такой проект, часто используется флаг --outFile, который склеивает все файлы в один большой на выходе.
Почему модули лучше?
Сравним два подхода:
-
Модули (ES6). Современный стандарт. Явные зависимости через
import/export. Отлично работает с инструментами сборки (Webpack, Vite, Rollup). Поддерживают tree-shaking (удаление неиспользуемого кода). Подавляющее большинство новых проектов используют именно их. -
Namespace. Устаревший подход, специфичный для TypeScript. Неявные зависимости через
/// <reference />. Требуют склейки файлов. Нет tree-shaking.
Вывод: Всегда используйте модули ES6 для новых проектов. Namespace стоит знать только для поддержки legacy-кода.
Важные нюансы при работе с модулями
1. Совмещенный импорт значений и типов
TypeScript очень умный. Когда вы пишете импорт, он понимает, импортируете вы тип или значение. Более того, на этапе компиляции в JavaScript все импорты типов просто удаляются, так как в JS их не существует.
Это позволяет писать очень лаконичный код:
import { User, createUser } from './types'; // User - тип, createUser - функция // TypeScript видит разницу и корректно обрабатывает оба случая. const user: User = createUser('Alice');
Начиная с TypeScript 4.5, можно явно указать, что импортируется только тип, с помощью ключевого слова type. Это может помочь избежать определенных цикличных зависимостей и сделать код яснее.
// Явное указание, что мы импортируем только тип import { createUser, type User } from './types';
2. Изоляция модулей и скриптовые файлы
По умолчанию TypeScript рассматривает файлы с импортом/экспортом как модули. Файлы без каких-либо импортов/экспортов считаются скриптами, которые работают в глобальной области видимости. Это может быть источником ошибок, если вы ожидаете, что ваш файл изолирован, но забыли добавить в него import или export. Чтобы всегда быть в безопасности, можно добавить в конфигурации tsconfig.json опцию "module": "ESNext" (или другую, кроме None) и тогда для изоляции можно использовать просто верхнеуровневый export {};.
// Файл: some-utility.ts // Даже если тут нет реального экспорта, эта строка делает файл модулем. export {}; // Теперь переменная `val` не попадет в глобальную область видимости. const val = 100;
3. Re-export (Реэкспорт)
Часто бывает удобно создать файл-баррель (barrel file), например, index.ts, в папке, который реэкспортирует все сущности из этой папки. Это упрощает импорт.
Допустим, у нас есть структура:
/utils/
formatters.ts
validators.ts
index.ts <- barrel file
В index.ts мы пишем:
// Реэкспортируем все из formatters export * from './formatters'; // Реэкспортируем все из validators export * from './validators'; // Или реэкспортируем конкретные сущности export { formatDate } from './formatters'; export { isEmailValid } from './validators';
Тогда в другом месте кода мы можем импортировать всё из одной точки:
import { formatDate, isEmailValid } from '../utils'; // Импорт из папки, ищется index.ts
Это очень чисто и удобно.
Практические задачи для закрепления
Задача 1: Рефакторинг
У вас есть файл monolith.ts с следующим содержимым:
interface Product { id: number; name: string; price: number; } class Cart { items: Product[] = []; addProduct(product: Product) { ... } getTotal(): number { ... } } function calculateTax(amount: number): number { ... } const TAX_RATE = 0.2; const user = { name: 'Максим' };
Разбейте этот код на логические модули:
-
types/product.ts(для интерфейсаProduct) -
classes/Cart.ts(для классаCart, ему потребуется импортироватьProduct) -
utils/tax.ts(для функцииcalculateTaxи константыTAX_RATE) -
data/user.ts(для константыuser)
Напишите код для каждого файла, не забывая про экспорт и импорт.
Задача 2: Создание модуля утилит
Создайте модуль arrayUtils.ts. Экспортируйте из него следующие функции и типы:
-
Тип
Predicate<T>(это функция(item: T) => boolean). -
Функцию
filterArray<T>(arr: T[], predicate: Predicate<T>): T[]. -
Функцию
mapArray<T, U>(arr: T[], mapper: (item: T) => U): U[].
Импортируйте и используйте эти функции в основном файле, чтобы отфильтровать и преобразовать массив чисел.
Задача 3: Barrel File
Создайте папку models. Поместите в нее файлы:
-
user.ts(с интерфейсомUserи типомUserId) -
post.ts(с интерфейсомPost)
Создайте в папкеmodelsфайлindex.ts, который реэкспортирует все типы изuser.tsиpost.ts. Импортируйте типыUserиPostв основном файле через barrel:import { User, Post } from './models';.
Заключение
Вы только что освоили модульную архитектуру. Умение грамотно разбивать код на файлы, экспортировать и импортировать не только реализации, но и типы, это признак профессионала.
Сначала это может показаться избыточным, но поверьте моему опыту, начиная с среднего размера проекта, это окупается стократно.
На следующем уроке мы поговорим о еще более мощных инструментах для структурирования кода. До скорой встречи.
Это 36-й урок из полного курса с уроками по TypeScript для начинающих.
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


