Avatar ·

Ошибка TypeScript: Свойство 'X' не существует в типе 'Window' в React-приложении

Привет! Я разрабатываю React-приложение с использованием create-react-app и TypeScript. У меня возникла проблема: я пытаюсь обратиться к глобальному свойству `window.myCustomProp`, которое я добавил в объект `window` для интеграции со сторонним скриптом. Вот мой код: ```typescript // App.tsx const App: React.FC = () => { useEffect(() => { // Устанавливаем кастомное свойство window.myCustomProp = 'Hello from custom property'; // Пытаемся прочитать его console.log(window.myCustomProp); }, []); return
My App
; }; ``` При компиляции TypeScript выдаёт ошибку: ``` Property 'myCustomProp' does not exist on type 'Window & typeof globalThis'. ``` Я уже пробовал использовать `(window as any).myCustomProp`, но это выглядит как костыль. Есть ли более правильный способ объявить кастомные свойства на объекте `window` в TypeScript? Я использую React 18, TypeScript 5.0 и create-react-app с последней версией. Буду благодарен за подробное объяснение, так как я новичок в TypeScript и хочу понять, как правильно расширять встроенные типы. <<>>

Проблема: 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. Для небольших проектов достаточно первого решения с расширением интерфейса.