Покоряем `window` в TypeScript: как добавить своё свойство и не сломать код

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему добавление свойства в 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 ✅ Полная ★★☆

Блоги

Рекомендаций по книгам