TypeScript: как правильно использовать setTimeout (window vs Node.js)?
Проблема: два разных 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 файлы в проекте). Этот подход особенно полезен в крупных проектах, где много работы с таймерами.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги