Как правильно использовать namespace вместе с внешними модулями в TypeScript?
Понимание разницы между namespace и модулями в TypeScript
Вы столкнулись с одной из самых частых проблем при переходе от глобального кода к модульной системе в TypeScript. Давайте разберёмся.
Ключевая идея: В современном TypeScript (начиная с версии 1.5) namespace и module (внешние модули) — это разные концепции, и их не следует смешивать. Когда вы используете import/export в файле, он автоматически становится внешним модулем. Внутри внешнего модуля использование namespace теряет смысл, потому что модуль сам по себе уже обеспечивает изоляцию и группировку кода.
Правильные решения
Решение 1: Использовать модули без namespace (рекомендуется)
Самый простой и современный подход — просто экспортировать функции/классы напрямую из модуля, без обёртки в namespace:
// math.ts
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 { add, subtract } from './math';
console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2
Если вы хотите сохранить группировку, можно использовать именованный импорт:
// app.ts
import * as MathUtils from './math';
console.log(MathUtils.add(5, 3));
console.log(MathUtils.subtract(5, 3));
Решение 2: Использовать namespace внутри модуля (легаси-подход)
Если по каким-то причинам вам нужно сохранить namespace (например, для обратной совместимости с глобальным кодом), можно сделать так:
// math.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 './math';
console.log(MathUtils.add(5, 3)); // Теперь работает!
Обратите внимание на export перед namespace. Это превращает namespace в экспортируемый объект, который можно импортировать.
Решение 3: Использовать объект-контейнер (альтернатива namespace)
Вместо namespace можно просто создать объект, который группирует функции:
// math.ts
export const MathUtils = {
add(a: number, b: number): number {
return a + b;
},
subtract(a: number, b: number): number {
return a - b;
}
};
// app.ts
import { MathUtils } from './math';
console.log(MathUtils.add(5, 3));
Когда использовать namespace?
- Глобальный код — если вы пишете скрипт без модульной системы (без import/export).
- Объявление типов для глобальных библиотек — в файлах
.d.tsдля описания глобальных переменных. - Внутренняя организация внутри одного файла — если файл очень большой и вы хотите логически сгруппировать код.
Рекомендация
Для новых проектов используйте только модули (экспорт/импорт). Namespace — это пережиток времён, когда TypeScript не поддерживал модули ES6. Современный TypeScript (и JavaScript) ориентируется на модульную систему. Если вам нужна группировка — используйте либо именованные импорты, либо объекты-контейнеры.
<<Дополнительный подход: Использование namespace для типов и интерфейсов
Если вы работаете с проектом, где namespace всё ещё используется для организации типов (например, в старом коде или при генерации Swagger/OpenAPI), вот как можно правильно импортировать типы из namespace, экспортированного из модуля:
// types.ts
export namespace ApiTypes {
export interface User {
id: number;
name: string;
}
export type Status = 'active' | 'inactive';
}
// app.ts
import { ApiTypes } from './types';
const user: ApiTypes.User = { id: 1, name: 'Alice' };
const status: ApiTypes.Status = 'active';
Также можно использовать import type для импорта только типов:
// app.ts
import type { ApiTypes } from './types';
// теперь ApiTypes будет удалён из скомпилированного JavaScript
Но ещё раз подчеркну: для нового кода лучше вообще отказаться от namespace в пользу прямого экспорта типов:
// types.ts
export interface User {
id: number;
name: string;
}
export type Status = 'active' | 'inactive';
// app.ts
import type { User, Status } from './types';
Это чище, проще для понимания и лучше поддерживается инструментами (IDE, линтеры, tree-shaking).