Namespaces
namespace, declare namespace, объединение объявлений
📖 Введение
Представьте большую библиотеку. В ней есть отделы: отдел научной литературы, отдел художественной литературы, отдел детских книг. В каждом отделе есть свои книги. Если вы ищете книгу по физике, вы идёте в отдел научной литературы и ищете там. Вы не будете искать её в отделе детских книг — это было бы неэффективно и запутанно.
Namespaces (пространства имён) работают так же — они группируют связанные имена в отдельные «отделы». Это помогает избежать конфликтов имён и делает код более организованным.
Важно: Namespaces — это устаревший механизм TypeScript. В современном коде используются ES-модули (import/export). Но namespaces до сих пор встречаются в старых проектах, в DefinitelyTyped (репозитории типов для JavaScript-библиотек), и иногда в declaration файлах. Поэтому важно понимать, как они работают.
1 Зачем нужны namespaces — проблема конфликтов
Без namespaces (или модулей) все имена живут в одной глобальной области. Это приводит к конфликтам:
// utils.ts
function formatDate(date: Date): string {
return date.toLocaleDateString('ru-RU');
}
// validators.ts
function formatDate(input: string): Date {
return new Date(input);
}
Две функции formatDate — конфликт! Что если оба файла подключены к одной странице? Одна перезапишет другую.
Namespace решает эту проблему, создавая изолированную область:
// utils.ts
namespace Utils {
export function formatDate(date: Date): string {
return date.toLocaleDateString('ru-RU');
}
}
// validators.ts
namespace Validators {
export function formatDate(input: string): Date {
return new Date(input);
}
}
// Теперь конфликта нет:
Utils.formatDate(new Date()); // Работает
Validators.formatDate('2024-01-01'); // Работает
2 Объявление namespace — синтаксис и структура
Namespace объявляется с помощью ключевого слова namespace:
namespace MyNamespace {
// Всё внутри — приватно по умолчанию
const secret = 'я не доступен снаружи';
// Экспортируем то, что нужно наружу
export function publicFunction() {
console.log(secret); // Могу использовать приватные переменные
}
export interface User {
name: string;
age: number;
}
}
Ключевые правила:
- Всё внутри namespace приватно по умолчанию
- Чтобы сделать что-то доступным, добавьте
export - Namespace может содержать функции, классы, интерфейсы, типы, константы
- Имена namespace принято писать в PascalCase
- Namespace могут быть вложенными
3 Вложенность namespace — иерархическая структура
Namespace могут быть вложенными друг в друга, создавая иерархическую структуру:
namespace App {
export namespace Models {
export interface User {
id: number;
name: string;
}
export interface Product {
id: number;
title: string;
price: number;
}
}
export namespace Services {
export class UserService {
getUser(): Models.User {
return { id: 1, name: 'Алексей' };
}
}
export class ProductService {
getProduct(): Models.Product {
return { id: 1, title: 'Ноутбук', price: 50000 };
}
}
}
}
// Использование:
const service = new App.Services.UserService();
const user = service.getUser();
Или более компактный синтаксис с точечной нотацией:
namespace App.Models {
export interface User {
id: number;
name: string;
}
}
namespace App.Services {
export class UserService {
getUser(): App.Models.User {
return { id: 1, name: 'Алексей' };
}
}
}
Обратите внимание: при точечной нотации родительский namespace не нужен — TypeScript создаёт его автоматически.
4 Namespace в нескольких файлах — объединение
Один namespace может быть разбит на несколько файлов. TypeScript автоматически объединяет их:
// models/user.ts
namespace App.Models {
export interface User {
id: number;
name: string;
}
}
// models/product.ts
namespace App.Models {
export interface Product {
id: number;
title: string;
}
}
// services/user.ts
namespace App.Services {
export class UserService {
getUser(): App.Models.User {
return { id: 1, name: 'Алексей' };
}
}
}
Все три файла вносят вклад в один namespace App. Чтобы TypeScript знал об этом, нужно подключить все файлы в tsconfig.json или использовать /// <reference>:
// services/user.ts
/// <reference path="../models/user.ts" />
/// <reference path="../models/product.ts" />
namespace App.Services {
export class UserService {
getUser(): App.Models.User {
return { id: 1, name: 'Алексей' };
}
}
}
/// <reference path="..." /> — это тройной слэш-директива. Она говорит компилятору: «подключи этот файл перед компиляцией текущего».
В tsconfig.json нужно указать файлы:
{
"include": ["src/**/*.ts"]
}
5 declare namespace — описание глобальных библиотек
declare namespace используется для описания существующих глобальных переменных, которые не были написаны на TypeScript. Это часто встречается в .d.ts файлах:
// Описание глобальной переменной jQuery
declare namespace jQuery {
function ready(callback: () => void): void;
function ajax(settings: any): void;
interface JQuery {
text(): string;
text(value: string): JQuery;
html(): string;
html(value: string): JQuery;
val(): string;
val(value: string): JQuery;
addClass(className: string): JQuery;
removeClass(className: string): JQuery;
on(event: string, handler: (event: Event) => void): JQuery;
}
function $(selector: string): JQuery;
}
// Теперь TypeScript знает о структуре jQuery
// Вы можете использовать jQuery.$('.my-element').text()
// с полным автодополнением
Другой пример — Node.js:
// Описание глобального объекта process
declare namespace NodeJS {
interface Process {
env: { [key: string]: string | undefined };
exit(code?: number): void;
argv: string[];
cwd(): string;
}
}
declare const process: NodeJS.Process;
6 Namespace vs Module — что выбрать
Вот сравнение двух подходов:
Namespaces:
- Работают в рантайме — создают обычные JavaScript-объекты
- Не поддерживают tree-shaking
- Не требуют настройки сборщика
- Устарели — не рекомендуются для нового кода
- Встречаются в declaration файлах (.d.ts)
- Просты для небольших проектов
ES-модули (import/export):
- Стандарт JavaScript (ES2015+)
- Поддерживают tree-shaking
- Требуют сборщик (Webpack, Vite, esbuild)
- Рекомендуются для всего нового кода
- Явный экспорт/импорт — чёткая структура
- Лучше для больших проектов
7 Namespace и JavaScript — как компилируется
Namespace компилируется в обычный JavaScript-объект. Вот как это выглядит:
// TypeScript
namespace Utils {
export function formatDate(date: Date): string {
return date.toLocaleDateString('ru-RU');
}
export const PI = 3.14159;
}
// Скомпилированный JavaScript:
var Utils;
(function (Utils) {
function formatDate(date) {
return date.toLocaleDateString('ru-RU');
}
Utils.formatDate = formatDate;
Utils.PI = 3.14159;
})(Utils || (Utils = {}));
Namespace — это просто IIFE (Immediately Invoked Function Expression), который создаёт объект. Это работает везде — в браузере, в Node.js, без сборщика.
🛠 Практический пример: типизация глобальных объектов
Допустим, вы работаете с библиотекой, которая добавляет глобальный объект MyLib:
// mylib.d.ts
declare namespace MyLib {
function init(config: Config): void;
function getVersion(): string;
interface Config {
apiKey: string;
debug: boolean;
timeout: number;
}
class Logger {
constructor(prefix: string);
log(message: string): void;
warn(message: string): void;
error(message: string): void;
}
}
// Теперь TypeScript знает о MyLib:
MyLib.init({ apiKey: 'abc', debug: true, timeout: 5000 });
const version = MyLib.getVersion();
const logger = new MyLib.Logger('App');
logger.log('Запущено');
8 Namespace и модули — как они работают вместе
Namespace и модули могут использоваться вместе, но это не рекомендуется:
// utils.ts
export namespace MathUtils {
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
}
// app.ts
import { MathUtils } from './utils';
console.log(MathUtils.add(1, 2)); // 3
console.log(MathUtils.subtract(5, 3)); // 2
Но лучше использовать модули без namespace. Модули уже создают изолированную область — namespace не нужен.
⚠️ Типичные ошибки при работе с namespaces
1. Забыли export:
namespace Utils {
function formatDate(date: Date): string { // ❌ Приватная!
return date.toLocaleDateString();
}
}
Utils.formatDate(new Date()); // Ошибка: formatDate не доступна
2. Путаница с файлами:
// ❌ Namespace в файле с модулями (если в tsconfig.json module != none)
export namespace Utils {
export function formatDate() {}
}
// ✅ Если используете модули, не смешивайте с namespaces
3. Использование namespace в современном коде:
// ❌ Не рекомендуется — используйте модули
namespace Utils {
export function formatDate() {}
}
// ✅ Рекомендуется — используйте модули
export function formatDate() {}
9 Миграция с namespace на модули — пошаговый процесс
Если вы работаете со старым проектом и хотите перейти с namespace на модули:
Шаг 1: Убедитесь, что в tsconfig.json включены модули:
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "node"
}
}
Шаг 2: Замените namespace на экспорт:
// Было:
namespace Utils {
export function formatDate(date: Date): string {
return date.toLocaleDateString();
}
}
// Стало:
export function formatDate(date: Date): string {
return date.toLocaleDateString();
}
Шаг 3: Обновите импорты:
// Было:
Utils.formatDate(new Date());
// Стало:
import { formatDate } from './utils';
formatDate(new Date());
10 Namespace в DefinitelyTyped — реальные примеры
Вот как namespace используется в DefinitelyTyped для описания jQuery:
// @types/jquery/index.d.ts
declare namespace jQuery {
interface JQueryAjaxSettings {
url?: string;
method?: string;
data?: any;
success?: (data: any) => void;
error?: (jqXHR: JQueryXHR, textStatus: string) => void;
}
interface JQuery {
// Манипуляция DOM
html(): string;
html(htmlString: string): JQuery;
text(): string;
text(textString: string): JQuery;
val(): string | number | string[];
val(value: string | number | string[]): JQuery;
// CSS
css(propertyName: string): string;
css(propertyName: string, value: string | number): JQuery;
// Классы
addClass(className: string): JQuery;
removeClass(className: string): JQuery;
toggleClass(className: string): JQuery;
hasClass(className: string): boolean;
// События
on(events: string, handler: Function): JQuery;
off(events: string, handler?: Function): JQuery;
trigger(eventType: string): JQuery;
// Анимация
animate(properties: Record<string, number>, duration?: number): JQuery;
fadeIn(duration?: number): JQuery;
fadeOut(duration?: number): JQuery;
}
}
// Глобальная функция jQuery
declare function jQuery(selector: string | Element | JQuery): jQuery.JQuery;
// Псевдоним $
declare const $: typeof jQuery;
Это позволяет использовать jQuery в TypeScript с полной типизацией и автодополнением.
11 Namespace и глобальные переменные — изоляция
Namespace помогают изолировать глобальные переменные, чтобы они не конфликтовали:
// lib-a.ts
namespace LibA {
export function init() {
console.log('LibA инициализирована');
}
}
// lib-b.ts
namespace LibB {
export function init() {
console.log('LibB инициализирована');
}
}
// Теперь можно вызывать обе функции без конфликтов:
LibA.init(); // LibA инициализирована
LibB.init(); // LibB инициализирована
12 Namespace и типы данных — определение типов
Namespace могут содержать типы и интерфейсы:
// types.ts
namespace AppTypes {
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
export interface Product {
id: number;
title: string;
price: number;
category: string;
}
export type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered';
export interface Order {
id: number;
userId: number;
products: Product[];
total: number;
status: OrderStatus;
createdAt: Date;
}
}
// Использование типов:
function processUser(user: AppTypes.User): string {
return `${user.name} (${user.role})`;
}
function getOrderStatus(order: AppTypes.Order): string {
return order.status;
}
13 Namespace и классы — определение классов
Namespace могут содержать классы:
// database.ts
namespace Database {
export class Connection {
private isConnected: boolean = false;
constructor(private host: string, private port: number) {}
connect(): void {
console.log(`Подключение к ${this.host}:${this.port}`);
this.isConnected = true;
}
disconnect(): void {
console.log('Отключение от базы данных');
this.isConnected = false;
}
isAlive(): boolean {
return this.isConnected;
}
}
export class Query {
constructor(private connection: Connection) {}
execute(sql: string): any[] {
if (!this.connection.isAlive()) {
throw new Error('Нет подключения к базе данных');
}
console.log(`Выполнение запроса: ${sql}`);
return [];
}
}
}
// Использование:
const conn = new Database.Connection('localhost', 5432);
conn.connect();
const query = new Database.Query(conn);
const results = query.execute('SELECT * FROM users');
14 Namespace и декораторы — использование с декораторами
Namespace могут использоваться с декораторами для расширения функциональности:
// decorators.ts
namespace Decorators {
export function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Вызов ${propertyKey} с аргументами:`, args);
const result = originalMethod.apply(this, args);
console.log(`Результат ${propertyKey}:`, result);
return result;
};
}
export function Measure(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} выполнился за ${end - start}ms`);
return result;
};
}
}
// Использование:
class Calculator {
@Decorators.Log
add(a: number, b: number): number {
return a + b;
}
@Decorators.Measure
complexCalculation(): number {
// Тяжёлые вычисления
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
return sum;
}
}
15 Namespace и итераторы — перебор коллекций
Namespace могут содержать итераторы для удобного перебора коллекций:
// iterators.ts
namespace Iterators {
export class Range {
constructor(private start: number, private end: number, private step: number = 1) {}
[Symbol.iterator](): Iterator<number> {
let current = this.start;
const end = this.end;
const step = this.step;
return {
next(): IteratorResult<number> {
if (current <= end) {
const value = current;
current += step;
return { value, done: false };
}
return { value: undefined, done: true };
}
};
}
}
export class Fibonacci {
private prev: number = 0;
private curr: number = 1;
[Symbol.iterator](): Iterator<number> {
return {
next: (): IteratorResult<number> => {
const value = this.curr;
[this.prev, this.curr] = [this.curr, this.prev + this.curr];
return { value, done: false };
}
};
}
}
}
// Использование:
const range = new Iterators.Range(1, 10, 2);
for (const num of range) {
console.log(num); // 1, 3, 5, 7, 9
}
const fib = new Iterators.Fibonacci();
let count = 0;
for (const num of fib) {
console.log(num); // 1, 1, 2, 3, 5, 8, 13, ...
if (++count >= 10) break;
}
16 Namespace и утилиты — библиотека утилит
Namespace идеально подходят для создания библиотек утилит:
// utils.ts
namespace Utils {
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function camelCase(str: string): string {
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}
export function slugify(str: string): string {
return str
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
export function truncate(str: string, length: number): string {
if (str.length <= length) return str;
return str.slice(0, length) + '...';
}
export function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number
): T {
let timeoutId: ReturnType<typeof setTimeout>;
return ((...args: any[]) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
}) as T;
}
}
// Использование:
Utils.capitalize('hello'); // Hello
Utils.camelCase('my-long-name'); // myLongName
Utils.slugify('Hello World!'); // hello-world
Utils.truncate('Длинный текст', 10); // Длинный ...
✅ Итого
- namespace — механизм группировки имён, создаёт изолированную область
- Всё приватно по умолчанию — используйте
exportдля доступа снаружи - declare namespace — описание существующих глобальных объектов (часто в .d.ts файлах)
- Namespace могут быть вложенными и разбиты на несколько файлов
- Устаревший механизм — для нового кода используйте ES-модули (import/export)
- Встречается в DefinitelyTyped и старых проектах
Проверяем понимание
5 вопросов