Avatar ·

Зачем нужно ключевое слово `declare` в TypeScript? Объясните на примерах

Всем привет! Начал изучать TypeScript и наткнулся на ключевое слово `declare`. В официальной документации написано, что оно используется для "объявления переменных, функций и классов, которые существуют где-то ещё". Но я не совсем понимаю, зачем это нужно и как это работает на практике. Например, я пишу простой скрипт, который использует глобальную переменную `$` (jQuery). Я знаю, что она подключена через CDN в HTML, но TypeScript ругается, что `$` не определена. Я попробовал написать: ```typescript declare var $: any; ``` И ошибка исчезла. Но почему нельзя просто написать `var $: any;`? В чём разница? Если я напишу `let $: any;`, то это тоже сработает, но тогда TypeScript будет считать, что я создаю новую переменную, а не описываю уже существующую. Также я видел, что `declare` используется в `.d.ts` файлах. Зачем они вообще нужны, если можно просто написать всё в обычных `.ts` файлах? Объясните, пожалуйста, простыми словами, для чего именно служит `declare` и в каких случаях его нужно применять. Желательно с примерами кода, чтобы было понятно. Заранее спасибо! <<>>

Что такое `declare` и зачем он нужен?

Ключевое слово declare в TypeScript — это механизм объявления окружения (ambient declaration). Оно сообщает компилятору: "Эй, эта переменная, функция или класс существуют в рантайме, но TypeScript не должен генерировать для них JavaScript-код. Просто знай, что они есть, и не выдавай ошибки типов".

Главное отличие от обычного объявления (let, const, function) в том, что declare не создаёт никакого кода. Если вы напишете:

// Обычное объявление — TypeScript сгенерирует JS-код
var $: any; // В JS появится: var $;

// Объявление с declare — JS-код не генерируется
declare var $: any; // В JS ничего не добавится

Во втором случае вы просто описываете тип для уже существующего объекта (например, подключённого через CDN). Если бы вы использовали let $: any, TypeScript переопределил бы переменную, что может привести к конфликтам.

Где используется `declare`?

  • Глобальные переменные (например, $ от jQuery, process от Node.js, __DEV__ от Webpack).
  • Внешние библиотеки без типов — вы описываете их API вручную.
  • Файлы деклараций (.d.ts) — основной способ описания типов для JS-библиотек.

Примеры использования

1. Глобальные переменные из CDN

// global.d.ts
declare var $: (selector: string) => JQuery;

// main.ts — теперь можно использовать $ без ошибок
$('.my-class').hide();

2. Описание модуля без типов

// lodash.d.ts
declare module 'lodash' {
  export function chunk(array: T[], size: number): T[][];
}

// app.ts
import { chunk } from 'lodash'; // Работает, хотя у lodash нет типов

3. Расширение существующих типов

// Добавляем свойство к глобальному window
declare global {
  interface Window {
    myCustomVar: string;
  }
}

// Теперь можно писать:
window.myCustomVar = 'hello';

Почему `.d.ts` файлы?

Файлы с расширением .d.ts — это "чистые" декларации. Они содержат только declare и не могут содержать реализаций. Это контракт: "Вот как выглядит API библиотеки, а реализация уже есть в JS". Обычные .ts файлы — это код, который компилируется. .d.ts — это только типы.

Итог: declare — это мост между миром JavaScript (где типов нет) и TypeScript (где типы обязательны). Он позволяет использовать существующий JS-код с полной проверкой типов, не генерируя лишнего кода.

<<>>

Дополнение: `declare` и модули

Важно понимать, что declare работает по-разному в глобальном контексте и внутри модулей. Если ваш файл — модуль (есть import или export), то объявление declare var $ не сделает $ глобальной. Вам нужно использовать declare global.

Пример для модуля:

// utils.ts (это модуль)
export function doSomething() {
  // Ошибка! $ не определена в этом контексте
  // $('div').hide();

  // Правильно: расширяем глобальный скоуп
  declare global {
    var $: (selector: string) => any;
  }
  // Теперь $ доступна
  $('div').hide();
}

Также declare часто используется с namespace для организации сложных типов:

declare namespace MyLib {
  function init(): void;
  interface Config {
    debug: boolean;
  }
}

Это позволяет вызывать MyLib.init() без импорта, если библиотека подключена глобально. Главное правило: если сущность существует в рантайме, но TypeScript о ней не знает — используйте declare.