Ошибка TypeScript: Свойство 'X' не существует в типе 'Window' в React-приложении
Проблема: TypeScript не знает о ваших кастомных свойствах
Когда вы добавляете новое свойство к объекту window, TypeScript не знает о нём, потому что тип Window уже определён в стандартной библиотеке типов. Ошибка возникает из-за того, что TypeScript проверяет соответствие кода объявленным типам, а window.myCustomProp не описан ни в одном из доступных определений.
Решение: Расширение интерфейса Window через declaration merging
TypeScript поддерживает механизм слияния объявлений (declaration merging). Вы можете расширить интерфейс Window, добавив своё свойство. Самый простой способ — создать файл объявлений (.d.ts) или объявить интерфейс прямо в вашем компоненте.
Вариант 1: Глобальное расширение через файл global.d.ts
Создайте файл src/global.d.ts (если его нет) и добавьте:
// src/global.d.ts
interface Window {
myCustomProp?: string;
}
Теперь TypeScript будет знать, что window.myCustomProp существует и имеет тип string | undefined. Ваш код будет работать без ошибок:
window.myCustomProp = 'Hello'; // OK
console.log(window.myCustomProp); // OK
Вариант 2: Локальное расширение в модуле
Если вы не хотите глобально изменять тип Window, можно расширить его только в рамках одного файла:
// App.tsx
declare global {
interface Window {
myCustomProp?: string;
}
}
const App: React.FC = () => {
useEffect(() => {
window.myCustomProp = 'Hello'; // TypeScript не ругается
}, []);
// ...
};
Почему это работает?
- Declaration merging — механизм TypeScript, который позволяет объединять несколько объявлений одного интерфейса в одно. Когда вы объявляете
interface Window { ... }, TypeScript сливает его с существующим интерфейсом из стандартной библиотеки. - В файле
.d.tsобъявление интерфейса автоматически считается глобальным, если не используетсяexport/import. - В модульном файле (с import/export) нужно явно указать
declare global, чтобы расширить глобальный тип.
Дополнительные рекомендации
- Используйте опциональное свойство (
?:), так как при первом обращении свойство может бытьundefined. - Если свойство обязательно, используйте
!(non-null assertion):window.myCustomProp!.toUpperCase(). - Для сложных объектов создавайте отдельный интерфейс:
// global.d.ts
interface MyCustomConfig {
apiKey: string;
debug: boolean;
}
interface Window {
myConfig?: MyCustomConfig;
}
Альтернатива: Использование as (не рекомендуется)
Хотя (window as any).myCustomProp работает, это плохая практика, потому что:
- Вы теряете все преимущества типизации.
- Ошибки будут выявляться только в рантайме.
- Код становится менее читаемым для других разработчиков.
Используйте расширение интерфейса — это правильный TypeScript-путь, который сохраняет строгую типизацию и делает код предсказуемым.
<<Другой подход: Использование модуля-обёртки
Если вы не хотите загрязнять глобальную область видимости, можно создать отдельный модуль, который будет управлять кастомными свойствами окна. Это улучшает инкапсуляцию и тестируемость.
Пример: windowService.ts
// windowService.ts
export const setCustomProp = (value: string): void => {
(window as Window & { myCustomProp?: string }).myCustomProp = value;
};
export const getCustomProp = (): string | undefined => {
return (window as Window & { myCustomProp?: string }).myCustomProp;
};
Использование в компоненте:
// App.tsx
import { setCustomProp, getCustomProp } from './windowService';
useEffect(() => {
setCustomProp('Hello');
console.log(getCustomProp());
}, []);
Плюсы подхода
- Типизация изолирована в одном месте.
- Легко мокировать в тестах — просто замените импорт.
- Не требуется глобальных объявлений.
Минусы
- Больше кода для простых случаев.
- Всё равно использует
asвнутри модуля.
Этот подход подходит, если вы работаете в команде и хотите чётко контролировать, какие свойства добавляются к window. Для небольших проектов достаточно первого решения с расширением интерфейса.