Avatar ·

TypeScript: как правильно использовать setTimeout (window vs Node.js)?

Всем привет! Я только начал осваивать TypeScript и столкнулся с непонятной проблемой при использовании `setTimeout`. Пишу простой таймер для веб-приложения, но TypeScript ругается на типы. Вот мой код: ```typescript const timerId = setTimeout(() => { console.log("Прошло 3 секунды"); }, 3000); clearTimeout(timerId); ``` TypeScript выдаёт ошибку: "Argument of type 'number' is not assignable to parameter of type 'Timeout'". Я в замешательстве. В JavaScript `setTimeout` всегда возвращал число (числовой ID таймера), и я спокойно передавал его в `clearTimeout`. Почему в TypeScript это не работает? Я погуглил и выяснил, что есть два разных `setTimeout`: один для браузера (window.setTimeout) и один для Node.js (global.setTimeout). Они возвращают разные типы: в браузере это `number`, а в Node.js — объект `NodeJS.Timeout`. Теперь я не понимаю, как заставить TypeScript использовать правильную версию. Моя цель — написать код, который будет работать и в браузере, и в Node.js (для тестирования). Как мне правильно типизировать `timerId`, чтобы код был универсальным? Или нужно писать разные реализации для разных сред? Буду благодарен за понятное объяснение с примерами. <<>>

Проблема: два разных setTimeout и конфликт типов

Вы абсолютно правы: в JavaScript существует две реализации setTimeout — одна в глобальном объекте window (браузер), другая в global (Node.js). Они возвращают разные типы:

  • Браузер (window): возвращает number (числовой идентификатор таймера).
  • Node.js (global): возвращает NodeJS.Timeout (специальный объект).

Причина ошибки в том, что TypeScript по умолчанию может подхватывать обе декларации, и возникает неоднозначность. Компилятор видит, что clearTimeout ожидает NodeJS.Timeout, а вы передаёте number.

Решение №1: универсальный тип ReturnType (рекомендуется)

Лучший способ — не привязываться к конкретному типу, а использовать ReturnType для автоматического выведения типа возвращаемого значения setTimeout. Это работает и в браузере, и в Node.js:

// Универсальный тип для хранения ID таймера
type TimerId = ReturnType<typeof setTimeout>;

let timerId: TimerId;

// Устанавливаем таймер
timerId = setTimeout(() => {
    console.log("Прошло 3 секунды");
}, 3000);

// Очищаем таймер
clearTimeout(timerId); // Ошибок нет!

Как это работает: ReturnType<typeof setTimeout> автоматически определит, какой тип возвращает setTimeout в вашей текущей среде. В браузере это будет number, в Node.js — NodeJS.Timeout. clearTimeout принимает оба варианта, так как он перегружен для обоих типов.

Решение №2: явное указание типа для конкретной среды

Если вы точно знаете, что код будет работать только в браузере (или только в Node.js), можно явно указать тип:

// Для браузера:
let timerId: number;

// Для Node.js:
let timerId: NodeJS.Timeout;

// Пример для браузера:
const timerId: number = window.setTimeout(() => {
    console.log("Браузерный таймер");
}, 1000);
clearTimeout(timerId); // Работает

Но такой подход менее гибкий и нарушает кроссплатформенность.

Решение №3: кастомный тип-обёртка

Можно создать свой тип, который будет работать в обеих средах:

// Создаём общий тип
type TimerHandle = number | NodeJS.Timeout;

let timerHandle: TimerHandle;

timerHandle = setTimeout(() => {
    console.log("Универсальный таймер");
}, 2000);

// Приводим к нужному типу при очистке
clearTimeout(timerHandle as Parameters<typeof clearTimeout>[0]);

Однако это менее элегантно, чем ReturnType.

Что делать, если код должен работать в обеих средах?

Если вы пишете библиотеку или код, который запускается и в браузере, и в Node.js (например, с помощью тестов), используйте решение №1 с ReturnType. Это наиболее идиоматичный способ для TypeScript.

Дополнительный совет: Если вы используете setInterval (который имеет ту же проблему), применяйте аналогичный подход:

type IntervalId = ReturnType<typeof setInterval>;
const intervalId: IntervalId = setInterval(() => {}, 1000);
clearInterval(intervalId);

Таким образом, ваш код будет типобезопасным и кроссплатформенным.

<<>>

Альтернативный подход: использование глобального объявления типа

Если вы часто сталкиваетесь с этой проблемой и хотите унифицировать типы для всего проекта, можно создать глобальное объявление типа в файле globals.d.ts:

// globals.d.ts
declare type TimerId = ReturnType<typeof setTimeout>;

После этого вы сможете использовать TimerId везде без дополнительных импортов:

const myTimer: TimerId = setTimeout(() => {}, 5000);
clearTimeout(myTimer);

Почему это может быть удобно?

  • Единообразие: Все разработчики в команде используют один и тот же тип.
  • Простота: Не нужно каждый раз писать ReturnType<typeof setTimeout>.
  • Поддержка: Если в будущем TypeScript изменит типы для setTimeout, ваше объявление автоматически подстроится.

Важное замечание

Убедитесь, что файл globals.d.ts включён в tsconfig.json (обычно TypeScript автоматически подхватывает все .d.ts файлы в проекте). Этот подход особенно полезен в крупных проектах, где много работы с таймерами.