Пространства имён в TypeScript
Пространство имён — это именованный контейнер для связанного кода. Оно объединяет типы, функции и классы под одним именем, чтобы они не конфликтовали с другим кодом.
Как устроены пространства имён в TypeScript
Пространства имён в TypeScript (раньше их называли «внутренними модулями») позволяют организовать код и избежать конфликтов имён, помещая связанную функциональность внутрь именованного контейнера.
Они помогают структурировать большие кодовые базы и держать область видимости под контролем — чисто и удобно для сопровождения.
Ключевые понятия:
- Логическая группировка: объединение связанного кода в именованные контейнеры
- Управление областью видимости: контроль того, какие части кода видны снаружи
- Предотвращение конфликтов имён: отсутствие конфликтов между сущностями с похожими именами
- Организация кода: построение иерархии в больших приложениях
Когда использовать пространства имён:
- Организация кода в больших устаревших приложениях
- Работа с глобальными библиотеками
- Миграция со старых кодовых баз на JavaScript
- Работа с кодом, который должен быть доступен глобально
Примечание: пространства имён по-прежнему полностью поддерживаются, но в современных приложениях обычно используют ES-модули (import/export), потому что модули дают лучшую модульность и позволяют сборщикам удалять неиспользуемый код (tree-shaking). Тем не менее понимание пространств имён полезно для сопровождения устаревших кодовых баз и в некоторых сценариях разработки библиотек.
Базовый синтаксис пространства имён
Пространство имён объявляется ключевым словом namespace, за которым следует блок. Снаружи блока доступны только члены, помеченные export; всё остальное остаётся приватным для пространства имён.
Пример
Пространство имён Validation экспортирует интерфейс и два класса, а lettersRegexp остаётся приватным для него. Снаружи к классам обращаются через точечную запись: Validation.LettersValidator.
namespace с экспортированными и приватными членами.
Объявите пространство имён Validation, которое экспортирует интерфейс StringValidator (isValid(s: string): boolean), содержит неэкспортированную константу lettersRegexp (/^[A-Za-z]+$/) и экспортирует два класса, реализующих этот интерфейс: LettersValidator (только буквы) и ZipCodeValidator (только цифры и ровно пять символов). Вне пространства имён создайте по одному объекту каждого класса через Validation.<Class> и выведите результат isValid для "Hello", "Hello123", "12345" и "1234".
Вывод: программа на TypeScript и захваченный вывод
namespace Validation {
// Всё внутри этого блока принадлежит пространству имён Validation
// Экспортируйте то, что должно быть доступно снаружи пространства имён
export interface StringValidator {
isValid(s: string): boolean;
}
// Это доступно только внутри пространства имён (не экспортировано)
const lettersRegexp = /^[A-Za-z]+$/;
// Экспортированный класс — доступен снаружи пространства имён
export class LettersValidator implements StringValidator {
isValid(s: string): boolean {
return lettersRegexp.test(s);
}
}
// Ещё один экспортированный класс
export class ZipCodeValidator implements StringValidator {
isValid(s: string): boolean {
return /^[0-9]+$/.test(s) && s.length === 5;
}
}
}
// Использование членов пространства имён
let letterValidator = new Validation.LettersValidator();
let zipCodeValidator = new Validation.ZipCodeValidator();
console.log(letterValidator.isValid("Hello")); // true
console.log(letterValidator.isValid("Hello123")); // false
console.log(zipCodeValidator.isValid("12345")); // true
console.log(zipCodeValidator.isValid("1234")); // false — неверная длина
Продвинутые возможности пространств имён
Пространства имён можно вкладывать друг в друга, выстраивая иерархию, а длинные пути к ним можно сокращать с помощью псевдонимов.
Вложенные пространства имён
App содержит два экспортированных внутренних пространства имён: Utils и Models. К членам обращаются по полному пути, например App.Utils.log; запись App.log была бы ошибкой, потому что log не является прямым членом App.
App, содержащее экспортированное пространство имён Utils (с функциями log и error, которые выводят [LOG]: msg и [ERROR]: msg) и экспортированное пространство имён Models (интерфейс User с полями id, name, email и класс UserService, метод getUser(id) которого возвращает John Doe / john@example.com). Вызовите App.Utils.log("Запуск приложения"), загрузите пользователя 1 через App.Models.UserService, выведите Пользователь загружен: <name> и оставьте закомментированный вызов App.log(...) с пометкой, что он был бы ошибкой.
Вывод: программа на TypeScript и захваченный вывод
namespace App {
export namespace Utils {
export function log(msg: string): void {
console.log(`[LOG]: ${msg}`);
}
export function error(msg: string): void {
console.error(`[ERROR]: ${msg}`);
}
}
export namespace Models {
export interface User {
id: number;
name: string;
email: string;
}
export class UserService {
getUser(id: number): User {
return { id, name: "John Doe", email: "john@example.com" };
}
}
}
}
// Использование вложенных пространств имён
App.Utils.log("Запуск приложения");
const userService = new App.Models.UserService();
const user = userService.getUser(1);
App.Utils.log(`Пользователь загружен: ${user.name}`);
// В TypeScript это было бы ошибкой типа
// App.log("прямое обращение к log"); // Ошибка — log не является прямым членом App
Псевдонимы пространств имён
Для пространства имён или для одного из его членов можно создать псевдоним с помощью import Alias = Some.Long.Path. Это не импорт модуля — так длинному имени просто даётся короткое. Components — псевдоним самого внутреннего пространства имён, а Button — псевдоним одного класса.
import Alias = Some.Long.Path.
Объявите VeryLongNamespace.DeeplyNested.Components (три вложенных пространства имён), экспортирующие классы Button и TextField, методы display() которых выводят Button отображён и TextField отображён. Создайте и отобразите кнопку тремя способами: через полный путь, через псевдоним Components самого внутреннего пространства имён и через псевдоним Button самого класса.
Вывод: программа на TypeScript и захваченный вывод
namespace VeryLongNamespace {
export namespace DeeplyNested {
export namespace Components {
export class Button {
display(): void {
console.log("Button отображён");
}
}
export class TextField {
display(): void {
console.log("TextField отображён");
}
}
}
}
}
// Без псевдонима — очень длинно
const button1 = new VeryLongNamespace.DeeplyNested.Components.Button();
button1.display();
// С псевдонимом пространства имён
import Components = VeryLongNamespace.DeeplyNested.Components;
const button2 = new Components.Button();
button2.display();
// С псевдонимом конкретного члена
import Button = VeryLongNamespace.DeeplyNested.Components.Button;
const button3 = new Button();
button3.display();
Пространства имён из нескольких файлов
В больших приложениях код часто разделён на несколько файлов. Пространство имён TypeScript может быть распределено по нескольким файлам и объединено во время компиляции с помощью комментариев-ссылок.
Комментарий-ссылка вида /// <reference path="validators.ts" /> сообщает TypeScript, что один файл зависит от другого, поэтому компилятор знает, в каком порядке их объединять. Четыре файла ниже вместе образуют одно пространство имён Validation.
Чтобы скомпилировать эти файлы в один файл JavaScript, выполните tsc --outFile sample.js main.ts.
validators.ts
Первый файл объявляет пространство имён Validation и экспортирует интерфейс StringValidator.
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
}
letters-validator.ts
Этот файл ссылается на validators.ts и дополняет то же пространство имён Validation классом LettersValidator.
/// <reference path="validators.ts" />
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersValidator implements StringValidator {
isValid(s: string): boolean {
return lettersRegexp.test(s);
}
}
}
zipcode-validator.ts
Этот файл тоже ссылается на validators.ts и добавляет в пространство имён класс ZipCodeValidator.
/// <reference path="validators.ts" />
namespace Validation {
const zipCodeRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isValid(s: string): boolean {
return zipCodeRegexp.test(s) && s.length === 5;
}
}
}
main.ts
Главный файл ссылается на все три файла, создаёт оба валидатора и выводит, подходит ли каждая проверяемая строка под каждый валидатор.
/// <reference path="validators.ts" />
/// <reference path="letters-validator.ts" />
/// <reference path="zipcode-validator.ts" />
// Now you can use the validators from multiple files
let validators: { [s: string]: Validation.StringValidator } = {};
validators["letters"] = new Validation.LettersValidator();
validators["zipcode"] = new Validation.ZipCodeValidator();
// Some samples to validate
let strings = ["Hello", "98052", "101"];
// Validate each
strings.forEach(s => {
for (let name in validators) {
console.log(`"${s}" - ${validators[name].isValid(s) ? "matches" : "does not match"} ${name}`);
}
});
Пространства имён и модули
Важно понимать, когда использовать пространства имён, а когда — ES-модули:
- Модули — предпочтительный способ организации кода в современных приложениях на TypeScript
- Пространства имён по-прежнему полезны в отдельных случаях, например при слиянии объявлений или работе с устаревшим кодом
- Модули лучше поддерживаются инструментами и допускают tree-shaking
- Пространства имён удобны для создания глобальных библиотек
Ниже — сравнение пространств имён и ES-модулей (import/export) по отдельным характеристикам:
- Рекомендуемая область применения и масштаб — Пространства имён: простые конфигурации, небольшие приложения, устаревшие кодовые базы. Модули: современные приложения любого размера; предпочтительны для новых проектов.
- Синтаксис и использование — Пространства имён: глобальный доступ через точечную запись (например,
MyNS.Member). Модули: явныеimport/exportс путями к файлам. - Загрузка и сборка — Пространства имён: загрузчик не нужен; можно получить один файл с помощью
--outFile. Модули: обычно проходят через сборщик или загрузчик (Vite, webpack и т. д.). - Разделение по файлам — Пространства имён: возможно с помощью комментариев
/// <reference />. Модули: естественно; каждый файл является модулем с явными экспортами и импортами. - Tree-shaking — Пространства имён: ограниченно; сборщикам трудно удалять неиспользуемый код. Модули: отлично; они и созданы для удаления мёртвого кода.
- Глобальная область видимости — Пространства имён: поощряют глобальные переменные (пусть и внутри пространства имён). Модули: избегают глобальных переменных; зависимости объявляются явно.
- Дополнение и слияние — Пространства имён: хорошая поддержка через слияние объявлений. Модули: дополнение модуля возможно, но с большими ограничениями.
- Инструменты и экосистема — Пространства имён: работают, но хуже согласуются с современными инструментами. Модули: лучшая поддержка со стороны современных инструментов и платформ.
- Лучше всего подходят для — Пространства имён: устаревшие библиотеки, глобальные скрипты, окружающие (ambient) типы. Модули: вся новая разработка, библиотеки и приложения.
Продвинутые приёмы работы с пространствами имён
С пространствами имён часто применяют ещё два приёма: дополнение существующего пространства имён через слияние объявлений и размещение внутри него обобщённых типов и классов.
Дополнение пространства имён
Второй блок declare namespace Express сливается с первым, добавляя requestTime и log в Request, а также новый интерфейс UserSession; код использования предполагает, что функция express() приходит из библиотеки.
// Original namespace
declare namespace Express {
interface Request {
user?: { id: number; name: string };
}
interface Response {
json(data: any): void;
}
}
// Later in your application (e.g., in a .d.ts file)
declare namespace Express {
// Augment the Request interface
interface Request {
// Add custom properties
requestTime?: number;
// Add methods
log(message: string): void;
}
// Add new types
interface UserSession {
userId: number;
expires: Date;
}
}
// Usage in your application
const app = express();
app.use((req: Express.Request, res: Express.Response, next) => {
// Augmented properties and methods are available
req.requestTime = Date.now();
req.log('Request started');
next();
});
Пространства имён с дженериками
Пространство имён DataStorage экспортирует обобщённый интерфейс Repository<T> и реализующий его класс InMemoryRepository<T>, который здесь используется с типом User.
// Generic namespace example
namespace DataStorage {
export interface Repository<T> {
getAll(): T[];
getById(id: number): T | undefined;
add(item: T): void;
update(id: number, item: T): boolean;
delete(id: number): boolean;
}
// Concrete implementation
export class InMemoryRepository<T> implements Repository<T> {
private items: T[] = [];
getAll(): T[] {
return [...this.items];
}
getById(id: number): T | undefined {
return this.items[id];
}
add(item: T): void {
this.items.push(item);
}
update(id: number, item: T): boolean {
if (id >= 0 && id < this.items.length) {
this.items[id] = item;
return true;
}
return false;
}
delete(id: number): boolean {
if (id >= 0 && id < this.items.length) {
this.items.splice(id, 1);
return true;
}
return false;
}
}
}
// Usage
interface User {
id: number;
name: string;
email: string;
}
const userRepo = new DataStorage.InMemoryRepository<User>();
userRepo.add({ id: 1, name: 'John Doe', email: 'john@example.com' });
const allUsers = userRepo.getAll();
Рекомендации
Делайте так:
- Используйте осмысленные иерархические имена пространств имён
- Экспортируйте из пространства имён только то, что действительно нужно
- Используйте комментарии
/// <reference />для упорядочивания файлов в пространствах имён из нескольких файлов - Рассматривайте модули для новых проектов
- Используйте
const enumвнутри пространств имён для лучшей производительности - Документируйте пространства имён комментариями JSDoc
Так не делайте:
- Не создавайте слишком глубокие иерархии пространств имён (более 2–3 уровней)
- Не засоряйте глобальную область видимости без необходимости
- Не смешивайте пространства имён и модули в одном проекте без чёткой стратегии
- Не используйте пространства имён в небольших приложениях — предпочитайте модули
Что учитывать с точки зрения производительности:
- Большие пространства имён могут увеличивать размер сборки
- Для больших приложений рассмотрите разделение кода на части
- Следите за циклическими зависимостями в сложных структурах пространств имён
- Используйте
const enumдля лучшей производительности с константными значениями
Переход от пространств имён к модулям
Переход от пространств имён к ES-модулям обычно означает, что каждое пространство имён превращается в отдельный файл с членами, помеченными export, а обращение через точку заменяется инструкциями import.
Шаги миграции:
- Превратите каждое пространство имён в файл-модуль
- Замените
exportвнутри пространства имён на экспорт ES-модуля - Обновите импорты, чтобы они использовали синтаксис ES-модулей
- Настройте систему сборки для работы с модулями
- Обновите тесты под новую структуру модулей
- Рассмотрите использование сборщика, например webpack или Rollup
- Обновите
tsconfig.json, указав"module": "ESNext"
Инструменты для миграции:
ts-migrate— инструмент автоматической миграции от Facebooktslintс правиломno-namespaceдля обнаружения пространств имён- Встроенные инструменты рефакторинга TypeScript
До и после миграции
Один и тот же класс UserService сначала находится внутри вложенных пространств имён, а затем становится экспортированным классом в собственном файле-модуле, который импортируется там, где нужен.
// Before: Using namespaces
namespace MyApp {
export namespace Services {
export class UserService {
getUser(id: number) { /* ... */ }
}
}
}
// After: Using ES modules
// services/UserService.ts
export class UserService {
getUser(id: number) { /* ... */ }
}
// app.ts
import { UserService } from './services/UserService';
const userService = new UserService();