“错误的setTimeout”:TypeScript如何让开发者抓狂
\n您是否曾经在TypeScript中编写过一个简单的定时器,然后代码编辑器突然开始把所有内容都标红?熟悉的场景:您导入setTimeout,编写const timer = setTimeout(...),然后TypeScript报错或提示一个难以理解的类型。问题出在哪里?原来setTimeout有两个版本——一个用于浏览器(window),另一个用于服务器(Node.js)。而追求完美的TypeScript迫使我们做出选择。
这个话题是Stack Overflow上最热门的话题之一。成千上万的开发者在同一个坑里跌倒。但我们不会只是简单地复制答案。我们将深入问题的根源,理解类型化的工作原理,并学会编写在浏览器和Node.js中都不会出错的代码。
\n\n为什么会有两个版本的setTimeout?
\n从历史上看,JavaScript存在于两个世界中:客户端(浏览器)和服务器端(Node.js)。在浏览器中,setTimeout是全局对象window的一个方法。在Node.js中,它是来自全局模块timers的一个函数。它们做同样的事情,但它们的类型签名不同。
以下是关键区别:
\n- \n
- 返回值:在浏览器中,
window.setTimeout返回一个number(定时器标识符)。在Node.js中,它返回一个NodeJS.Timeout对象。 \n - 上下文:在浏览器中,回调函数内部的
this是window(在非严格模式下)。在Node.js中,它是定时器对象。 \n - 类型化:TypeScript为不同的环境提供了不同的声明。如果您编写通用代码(同构应用),就会发生冲突。 \n
问题在代码中如何体现
\n假设您正在开发一个必须在客户端和服务器端都能运行的库。您编写了这样的代码:
\n// utils.ts\nexport function delay(ms: number): Promise<void> {\n return new Promise(resolve => {\n const timer = setTimeout(resolve, ms);\n // timer的类型是什么?number还是NodeJS.Timeout?\n });\n}\nTypeScript会发现setTimeout可能有两种类型,并报错:“类型 'Timeout' 不能赋值给类型 'number'。” 或者反过来,具体取决于tsconfig.json的设置和已包含的类型。
为了更直观,下面是通过ReturnType查看类型时的样子:
// 在浏览器中 (lib.dom.d.ts)\ntype BrowserTimer = ReturnType<typeof window.setTimeout>; // number\n\n// 在Node.js中 (@types/node)\ntype NodeTimer = ReturnType<typeof setTimeout>; // NodeJS.Timeout\n\n针对不同场景的正确解决方案
\n让我们看看四种方法。选择适合您项目的那一种。
\n\n1. 如果您只针对浏览器编写代码
\n最简单的情况。只需将类型指定为number并显式使用window.setTimeout:
// browser-timer.ts\nconst timerId: number = window.setTimeout(() => {\n console.log('一秒过去了');\n}, 1000);\n\n// 清除定时器\nwindow.clearTimeout(timerId);\n为什么这样有效?在浏览器环境中,window.setTimeout总是返回一个数字。如果您包含了lib: ["dom"],TypeScript知道这一点。
2. 如果您只针对Node.js编写代码
\n这里的类型是NodeJS.Timeout。直接使用它:
// node-timer.ts\nimport { setTimeout } from 'timers'; // 显式导入以保持清晰\n\nconst timer: NodeJS.Timeout = setTimeout(() => {\n console.log('定时器触发了');\n}, 2000);\n\nclearTimeout(timer); // clearTimeout接受NodeJS.Timeout\n重要提示:在Node.js中,clearTimeout也接受No