Введение: Почему добавление свойства в window — это головная боль для каждого TypeScript-разработчика?
Представьте ситуацию: вы пишете современное SPA-приложение на TypeScript. Всё идёт гладко, пока не возникает необходимость сохранить глобальное состояние или интеграцию с внешним скриптом (например, аналитикой, картами или платежным шлюзом). Вы пишете:
window.myAppConfig = { apiKey: '123' };
И тут TypeScript выдаёт вам красную ошибку: "Property 'myAppConfig' does not exist on type 'Window & typeof globalThis'". Знакомо? Этот вопрос собрал тысячи просмотров на Stack Overflow, и сегодня мы не просто дадим готовое решение, но и заглянем под капот TypeScript, чтобы понять, почему так происходит и как с этим бороться элегантно и безопасно.
В этой статье мы разберём несколько подходов — от «костылей» для новичков до профессиональных паттернов, которые используют senior-разработчики. Поехали!
Под капотом: почему TypeScript ругается на window.myProp?
Чтобы понять проблему, нужно вспомнить, что TypeScript — это надстройка над JavaScript, которая добавляет статическую типизацию. Объект window в браузере — это глобальный объект, который имеет заранее определённый тип (Window). Этот тип описан в стандартных библиотеках TypeScript (lib.dom.d.ts) и включает все стандартные свойства: document, location, setTimeout и сотни других.
Когда вы пытаетесь присвоить window.myProp, TypeScript проверяет: «Есть ли такое свойство в интерфейсе Window?». Нет — значит ошибка. Это защита от опечаток и случайного загрязнения глобальной области видимости. Но что делать, если вы намеренно хотите расширить window?
Способ 1: Самый простой, но опасный — as any
Первый вариант, который приходит в голову новичку — просто «заткнуть» TypeScript с помощью приведения типа:
(window as any).myAppConfig = { apiKey: '123' };
Плюсы: быстро, работает, не требует дополнительных определений.
Минусы: вы теряете все преимущества TypeScript. Никакой автоподстановки, никакой проверки типов. Если вы случайно напишете myAppConfigg — ошибки не будет, но приложение упадёт в рантайме. Это как выключить подушки безопасности в машине — ехать можно, но рискованно.
Когда использовать: только для быстрого прототипирования или если вы уверены, что свойство будет использоваться один раз и будет удалено.
Способ 2: Расширение глобального интерфейса (Interface Merging)
TypeScript поддерживает слияние интерфейсов (declaration merging). Это ключевой механизм, который позволяет «добавить» новые свойства к уже существующему типу Window. Для этого нужно объявить интерфейс с тем же именем в глобальной области видимости.
Создайте файл global.d.ts (или добавьте в любой .d.ts файл вашего проекта):
// global.d.ts
interface Window {
myAppConfig: {
apiKey: string;
debugMode?: boolean;
};
myAnalyticsFunction: (event: string, data: object) => void;
}
Теперь вы можете писать:
window.myAppConfig = { apiKey: '123' };
window.myAnalyticsFunction('page_view', { page: '/home' });
// TypeScript подскажет, что apiKey — это string
console.log(window.myAppConfig.apiKey); // ✅ Без ошибок
Как это работает под капотом? TypeScript автоматически объединяет (merge) все объявления интерфейсов с одинаковым именем. Если в lib.dom.d.ts уже есть interface Window { ... }, ваш интерфейс с тем же именем просто добавит к нему новые свойства. Это называется global augmentation.
Плюсы: полная типизация, автодополнение в IDE, безопасность.
Минусы: нужно помнить о создании отдельного файла. Если свойств много, интерфейс может разрастись.
Способ 3: Модульное расширение через declare global
Если ваш проект использует модули (ES-модули с import/export), просто объявить interface Window в корневом файле не сработает — TypeScript посчитает, что вы объявляете локальный интерфейс, а не расширяете глобальный. В этом случае используйте конструкцию declare global:
// utils/windowExtensions.ts
export {};
declare global {
interface Window {
myAppConfig: {
apiKey: string;
};
}
}
// Инициализация (можно вызвать один раз в точке входа)
window.myAppConfig = { apiKey: '123' };
Обратите внимание на export {} — это превращает файл в модуль. Без него declare global не сработает.
Совет профессионала: вынесите все глобальные расширения в отдельный файл types/global.d.ts и не забудьте включить его в tsconfig.json через "include" или "files".
Способ 4: Утилита-обёртка для безопасного доступа
Если вы не хотите каждый раз расширять интерфейс, можно создать вспомогательную функцию, которая обеспечит типизацию без загрязнения глобального объекта:
// utils/globalStore.ts
class GlobalStore {
private store: Record<string, unknown> = {};
set<T>(key: string, value: T): void {
this.store[key] = value;
}
get<T>(key: string): T | undefined {
return this.store[key] as T | undefined;
}
}
export const globalStore = new GlobalStore();
// Использование:
globalStore.set('apiKey', '123');
const apiKey = globalStore.get<string>('apiKey'); // string | undefined
Этот паттерн не использует window вообще, что делает код более тестируемым и безопасным. Но если вам обязательно нужно именно глобальное свойство (например, для интеграции с библиотекой, которая ожидает window.myLib), то вернитесь к способу 2 или 3.
Сравнительная таблица подходов
| Метод | Типобезопасность | Сложность | Когда использовать |
|---|---|---|---|
as any |
❌ Нет | ★☆☆ | Быстрый прототип |
interface Window |
✅ Полная | ★★☆
БлогиРекомендаций по книгамЧитайте также |