Короткий ответ: из замыкания в TypeScript лучше экспортировать не саму скрытую переменную, а функцию, фабрику или объект методов, которые получают доступ к этой переменной. Замыкание остается приватным, а наружу выходит типизированный API
Плохая формулировка задачи звучит так: “хочу экспортировать переменную изнутри функции”. Правильная формулировка: “хочу дать внешнему коду управляемый доступ к состоянию, которое хранится внутри замыкания”
Пример с приватным счетчиком
export function createCounter(initialValue = 0) {
let value = initialValue;
return {
increment(): number {
value += 1;
return value;
},
getValue(): number {
return value;
},
reset(): void {
value = initialValue;
},
};
}
Использование:
import { createCounter } from "./counter";
const counter = createCounter(10);
console.log(counter.increment());
console.log(counter.getValue());
Переменная value не экспортируется напрямую. Ее нельзя случайно переписать из другого файла, но методы increment, getValue и reset имеют к ней доступ
Типизируйте публичный API
Чтобы внешний контракт был явным, вынесите тип:
export type Counter = {
increment(): number;
getValue(): number;
reset(): void;
};
export function createCounter(initialValue = 0): Counter {
let value = initialValue;
return {
increment() {
value += 1;
return value;
},
getValue() {
return value;
},
reset() {
value = initialValue;
},
};
}
Теперь другой файл может импортировать и фабрику, и тип:
import { createCounter, type Counter } from "./counter";
const counter: Counter = createCounter();
Если нужно экспортировать готовый экземпляр
Иногда нужен один общий объект на весь модуль:
function createStore() {
let token: string | null = null;
return {
setToken(value: string) {
token = value;
},
getToken() {
return token;
},
clear() {
token = null;
},
};
}
export const authStore = createStore();
Так работает модульный singleton: состояние закрыто внутри createStore, а наружу экспортируется только управляемый объект
Если нужно экспортировать только функцию доступа
Иногда не нужен целый объект методов. Достаточно экспортировать функцию, которая сама держит замыкание:
let cachedConfig: { apiUrl: string } | null = null;
export function getConfig() {
if (!cachedConfig) {
cachedConfig = { apiUrl: "https://api.example.com" };
}
return cachedConfig;
}
Это тоже замыкание на уровне модуля: переменная cachedConfig не экспортируется, но функция getConfig управляет доступом к ней. Такой вариант удобен для lazy-init, кеширования настроек и простых сервисов
Если наружный код не должен менять объект, возвращайте копию:
return { ...cachedConfig };
Так вы снижаете риск, что другой модуль случайно испортит внутреннее состояние
Частые ошибки
Первая ошибка — пытаться написать export внутри функции. В TypeScript/ES modules экспорт делается на верхнем уровне модуля, а не внутри блока или функции
Вторая ошибка — вернуть ссылку на изменяемый внутренний объект без защиты. Если нужно сохранить инварианты, отдавайте копию или методы
Третья ошибка — использовать any, когда можно описать тип возвращаемого API. Тогда весь смысл TypeScript в этом месте теряется
Четвертая ошибка — путать export и return. return возвращает значение из функции, а export делает сущность доступной из другого модуля. Для замыкания обычно нужны оба механизма: export function createStore(), а внутри этой функции return { ... }
Самопроверка
Создайте createCounter, импортируйте его в другой файл и попробуйте обратиться к value напрямую. Это не должно быть возможно. Затем вызовите increment() и getValue() — эти методы должны работать. Значит, замыкание сохранено, а экспорт сделан правильно
Что почитать дальше по TypeScript
Если нужен общий маршрут по теме, откройте рубрику TypeScript. Для соседних задач пригодятся эти разборы:
- Как экспортировать и импортировать классы TypeScript из npm-пакета
- 10 вопросов по TypeScript и ответы на них
- Any, unknown, never и strict
- App.tsx: что это за файл и как добавить TypeScript в React