TypeScript - 使用正确版本的 setTimeout(node vs window)

369

我正在升级一些旧的TypeScript代码以使用最新的编译器版本,但在调用setTimeout时遇到了问题。该代码期望调用浏览器的setTimeout函数并返回一个数字:

setTimeout(handler: (...args: any[]) => void, timeout: number): number;

然而,编译器正在将其解析为Node实现,它返回一个NodeJS.Timer:

setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): NodeJS.Timer;

此代码无法在节点中运行,但是节点typing被作为其他东西(不确定是什么)的依赖项引入。

我该如何指示编译器选择我想要的setTimeout版本?

以下是相关的代码:

let n: number;
n = setTimeout(function () { /* snip */  }, 500);

这会产生编译错误:

TS2322:类型“计时器”无法分配给类型“数字”。


1
你的tsconfig.json文件中是否有types:["node"]?请参考https://dev59.com/klgQ5IYBdhLWcg3wUiZS#43952363。 - derkoe
@koe 不,我的 tsconfig 文件中没有 types:["node"] 选项。但是 Node 的类型作为其他 npm 依赖关系的一部分被引入了进来。 - Kevin Tighe
4
你可以在 tsconfig.json 中明确定义“类型” - 当省略“node”时,它不会在编译中使用。例如:“types”:[“jQuery”]。 - derkoe
3
@koe 的答案(使用 "types" 选项)是唯一真正正确的答案,令人惊讶的是它没有得到任何投票。 - Egor Nepomnyaschih
1
@KevinTighe的types不包括node,但是setTimeout仍然获取其Node类型而不是浏览器类型。types默认为node_modules/@types中的所有类型,如https://www.typescriptlang.org/tsconfig#types所述,但即使您指定了`types`并且没有包含“node”,为什么`setTimeout`仍然获取其Node类型,以及如何获取浏览器类型?@Axke的解决方案有点像一个hack,基本上是说它返回什么就是什么。TypeScript可能仍然会找到错误的类型,但至少它将始终保持一致性错误。 - Denis Howe
显示剩余2条评论
13个回答

524
let timer: ReturnType<typeof setTimeout> = setTimeout(() => { ... });

clearTimeout(timer);

通过使用ReturnType<fn>,您可以获得与平台的独立性。您将不会被强制使用anywindow.setTimeout,如果您在nodeJS服务器上运行代码(例如服务器端渲染页面),这些内容将会出现问题。


好消息是,这也与Deno兼容!


21
根据我的理解,这是正确的答案,应该被接受,因为它为支持setTimeout/ clearTimeout 的每个平台提供了正确的类型定义,并且没有使用any - afenster
18
如果你正在编写一个既可以在 NodeJS 上运行,也可以在浏览器上运行的库,这是解决方案。 - yqlim
5
如果直接使用 setTimeout,返回类型是 NodeJS.Timeout,如果使用 window.setTimeout,则返回类型为 number。不需要使用 ReturnType - cchamberlain
2
@cchamberlain,你需要它,因为你运行了setTimeout函数,并期望其结果被存储在变量中。在TS playground中自己尝试一下吧。 - Akxe
3
这个解决方案对我来说是正确的。如果不使用它,我的 Node 应用程序会在 TypeScript 编译时编译正确,但使用 Jest 单元测试时它会选择不正确的 window.setTimeout 定义。 - Simon Clough
显示剩余3条评论

283

2021更新

Akxe的回答提出了在Typescript 2.3中引入的ReturnType<Type>技术。

let n: ReturnType<typeof setTimeout>;
n = setTimeout(cb, 500);

这种方式看起来很好,似乎比显式转换更受欢迎。但在这种情况下,“n”的结果类型是“NodeJS.Timeout”,并且可以按如下方式使用:

let n: NodeJS.Timeout;
n = setTimeout(cb, 500);

使用 ReturnType/NodeJS.Timeout 方法的唯一问题是,在特定于浏览器的环境中进行数值运算仍然需要进行类型转换:

if ((n as unknown as number) % 2 === 0) {
  clearTimeout(n);
}

原始回答

一个不影响变量声明的解决方法:

let n: number;
n = setTimeout(function () { /* snip */  }, 500) as unknown as number;

此外,在特定于浏览器的环境中,可以直接使用window对象而无需进行强制转换:

let n: number;
n = window.setTimeout(function () { /* snip */  }, 500);

61
我认为另一个方法(window.setTimeout)应该是这个问题的正确答案,因为它是最清晰的解决方案。 - amik
12
如果您使用“any”类型,那么您并没有真正提供TypeScript的答案。 - S..
4
window.setTimeout 可能会导致单元测试框架(node.js)出现问题。最好的解决方案是使用 let n: NodeJS.Timeoutn = setTimeout - cchamberlain
const timer = useRef<ReturnType<typeof setInterval>>(); - Mounika Bathina
1
@AntonOfTheWoods 你应该可以再次将它作用域限定,但是使用 self 而不是 window https://dev59.com/frXna4cB1Zd3GeqPIEYf 。希望正确设置 TypeScript 将为其分配适当的类型,但我没有相关经验。 - amik
显示剩余3条评论

44

我猜这取决于您将在哪里运行您的代码。

如果您的运行目标是服务器端的Node JS,请使用:

let timeout: NodeJS.Timeout;
global.clearTimeout(timeout);

如果您的运行目标是浏览器,请使用:

let timeout: number;
window.clearTimeout(timeout);

21

对我来说,这很完美地运作。

type Timer = ReturnType<typeof setTimeout>

const timer: Timer = setTimeout(() => {}, 1000)

1
基本上与 https://dev59.com/DVcO5IYBdhLWcg3wcRNk#56239226 相同,该回答发布时间早于近两年。 - Can Rau
@CanRau 但对我来说仍然很有用。不知道为什么,但是我没能从其他答案中得到这个想法,尽管现在看来它并不是那么庞大。 - Dmitry Koroliov

14

这可能适用于旧版本,但对于TypeScript版本^3.5.3和Node.js版本^10.15.3,您应该能够从计时器模块导入特定于Node的函数,即:

import { setTimeout } from 'timers';

这将返回一个类型为NodeJS.TimeoutTimeout实例,您可以将其传递给clearTimeout

import { clearTimeout, setTimeout } from 'timers';

const timeout: NodeJS.Timeout = setTimeout(function () { /* snip */  }, 500);

clearTimeout(timeout);

5
同样地,如果你想要浏览器版本的setTimeout,可以使用类似于const { setTimeout } = window的语句来消除这些错误。 - Jack Steam

8

还想提一下,NodeJS.Timeout 的规范包括 [Symbol.toPrimitive](): number

interface Timeout extends Timer {
    /**
     * If true, the `Timeout` object will keep the Node.js event loop active.
     * @since v11.0.0
     */
    hasRef(): boolean;
    /**
     * Sets the timer's start time to the current time, and reschedules the timer to
     * call its callback at the previously specified duration adjusted to the current
     * time. This is useful for refreshing a timer without allocating a new
     * JavaScript object.
     *
     * Using this on a timer that has already called its callback will reactivate the
     * timer.
     * @since v10.2.0
     * @return a reference to `timeout`
     */
    refresh(): this;
    [Symbol.toPrimitive](): number;
}

另外,为了兼容性,Node中的其他超时API与普通整数ID一起使用正常工作,它们不需要接受对象。对象在服务器端用于允许对保持进程活动和垃圾收集等进行更精细的控制。例如:

function clearTimeout(timeoutId: NodeJS.Timeout | string | number | undefined): void;

这意味着您可以对setTimeoutsetInterval的结果使用原始转换:
let timeoutId: number | undefined;
timeoutId = Number(setTimeout(callback, ms));

function clear() {
  clearTimeout(timeoutId);
}

如果你需要依赖于该值作为某个其他API契约的基本类型,那么它既不会与任何API冲突,又不会在以后导致类型问题。


谢谢,Number()帮助我将其转换为数字。 - Thykof

2

如果你的代码没有在node中运行,并且节点类型来自依赖项。

  • 如果尚未安装,请安装@types/web

  • 在您的项目中创建一个文件,例如web-types.d.ts

  • 在文件顶部添加以下行 /// <reference types="web" />

  • compilerOptions下添加 "typeRoots": ["./web-types", "./node_modules/@types"]

这将优先考虑浏览器类型而不是节点类型。


// <reference types="web" /> 在 'web' 上显示错误 "无法找到 'web' 的类型定义文件"我通过 npm install @typescript/lib-dom@npm:@types/web --save-dev 安装了 @type/web。 - klau
添加 @types/web 立刻解决了这个问题。 - Joel Stransky

1
如果您的目标是windowsetInterval。那么你也可以这样写。
let timerId: number = setInterval((()=>{
    this.populateGrid(true)
  }) as TimerHandler, 5*1000)
}

0

我通过设置解决了这个问题。

tsconfig.json:
{
  "compilerOptions": {
    "skipLibCheck": true,
  }
}

创建 .d.ts 文件
*.d.ts:
declare namespace NodeJS {
    type Timeout = number;
    type Timer = number;
}

typescript 版本 4.2.3


0

我正在使用RTL测试我的计数器应用程序,特别是在测试一个元素是否在计数达到15时被删除。由于组件在运行测试后被销毁,因此setTimeout仍然会运行,并抛出错误,说React无法对未安装的组件执行状态更新。所以,根据dhilt的答案,我能够以这种方式修复我的useEffect清理函数:

const [count, setCount] = useState(initialCount);
const [bigSize, setBigSize] = useState(initialCount >= 15);

useEffect(() => {
    let id: NodeJS.Timeout;

    if(count >= 15) {
        id = setTimeout(() => setBigSize(true), 300);
    }

    return function cleanup() {
        clearTimeout(id);
    }
});

以下是测试套件:

describe('when the incrementor changes to 5 and "add" button is clicked', () => {
        beforeEach(async () => {
            userEvent.type(screen.getByLabelText(/Incrementor/), '{selectall}5');
            userEvent.click(screen.getByRole('button', {name: "Add to Counter"}));
            await screen.findByText('Current Count: 15');
        })
            
        it('renders Current Count: 15', () => {
            expect(screen.getByText('Current Count: 15')).toBeInTheDocument();
        });
        
        it('renders too big and will dissapear after 300ms',async() => {
            await waitForElementToBeRemoved(() => screen.queryByText(/size: small/i))
        });
        
    })

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接