Skip to content

记一次在 Typescript 中给 debounce 写注解

Published at  at 11:40 PM

背景

最近遇到一个 debounce 场景,我熟练的找到之前的 js 代码:

export const debounce = (fn, ms = 300) => {
  let timer;
  return function debounced(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.call(this, ...args);
    }, ms);
  };
};

然后随便改了改:

export const debounce = (fn: Function, ms: number = 300): Function => {
  let timer: number;
  return function debounced(...args: any[]) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.call(this, ...args);
    }, ms);
  };
};

问题

然后 vscode 就划了两道红线: image image

1

第一个还好,搜了一下,因为 Node 里 setTimeout 返回值跟浏览器不一样。我看有些答案说让改成 window.setTimeout,我第一时间就照做了,显然这样一点都不优雅,没有兼容 nodejs 的使用场景。Typescript 有个关键字typeof,可以取到 setTimeout 的类型,然后再配合 ReturType,轻松取到 setTimeout 返回值:

let timeoutId = ReturnType<typeof setTimeout>;

2

第二个问题,简单,ThisParameterType 嘛,取 fn 的 this 类型,顺便加个泛型:

type NoReturFn = (this: any, ...args: any[]) => void;
export function debounce<F extends NoReturFn>(fn: F, ms: number = 300): F {
  let timeoutId: ReturnType<typeof setTimeout>;
  return function debounced(this: ThisParameterType<F>, ...args: any[]) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      fn.call(this, ...args);
    }, ms);
  };
}

一保存,又飘红了:

不能将类型“(this: ThisParameterType<F>, ...args: any[]) => void”分配给类型“F”。
  "(this: ThisParameterType<F>, ...args: any[]) => void" 可赋给 "F" 类型的约束,但可以使用约束 "NoReturFn" 的其他子类型实例化 "F"。ts(2322)

我:???F 不是约束了就是这种格式吗? 到这里我其实想了老半天,中间搜过别人的实现,看过@types/lodash。但他们写的都太应付了,returnType 写的都是 Function,这不坐实AnyScript? 一时半会儿没能解决,心态有点崩:

- return function debounced...
+ return <F>function debounced...

2.1

这样确实消除了警告,但我越想越不服,凭啥不能分配给 F? 我猜想可能是 F 并不一定会遵守 NoReturnFn 的格式(指 return void,我试了下有 return 值的函数确实可以传进去且不报错:

const wtf: () => void = () => 1;

但是这样就是可以(符合直觉地)报错,完全搞不懂:

const wtf1: () => undefined | void = () => 1;

后来翻到这么一句话:

How to ensure a generic function passed to a higher order function has void return type? I think the best thing to do here is probably to embrace TypeScript’s viewpoint that a void return type means “ignore any value returned” and not “no value is returned”, and move on.

大概明白了,但是为啥我在官方文档里没看到过…

所以这个 NoReturnFn 是没有效果的,除非改成:

- type NoReturnFn = (this: any, ...args: any[]) => void;
+ type NoReturnFn = (this: any, ...args: any[]) => undefined | void

但是这样的话,() => undefined 也是被允许的了…所以好像是没有完美的 void return 写法了

3

但 debounced 的 return 值一定是 void,顺着这个思路,又改了一版:

type NoReturnFn = (this: any, ...args: any[]) => void;
export function debounce<F extends NoReturnFn>(
  fn: F,
  ms: number = 300
): (this: ThisParameterType<F>, ...args: any[]) => void {
  let timeoutId: ReturnType<typeof setTimeout>;
  return function debounced(this: ThisParameterType<F>, ...args: any[]) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      fn.call(this, ...args);
    }, ms);
  };
}

好了,没有警告,没有报错,暂时就这样了。


优化

1

后来又了解到:泛型的参数和函数的参数并不需要一一对应,精简了下:

export function debounce<T, P extends any[]>(fn: (this: T, ...p: P) => undefined | void, ms: number = 300) {
  let timeoutId: ReturnType<typeof setTimeout>;
  return function debounced(this: T, ...args: P) {
    if (timeoutId !== void 0) {
      clearTimeout(timeoutId);
    }
    timeoutId = setTimeout(() => {
      fn.call(this, ...args);
    }, ms);
  };
}

2

return undefined 也去掉:

How to ensure a generic function passed to a higher order function has void return type?

export function debounce<T, P extends any[], R>(
  fn: (this: T, ...p: P) => R & (void extends R ? void : never),
  ms: number = 300
) {
  let timeoutId: ReturnType<typeof setTimeout>;
  return function debounced(this: T, ...args: P) {
    if (timeoutId !== void 0) {
      clearTimeout(timeoutId);
    }
    timeoutId = setTimeout(() => {
      fn.call(this, ...args);
    }, ms);
  };
}

新手上路,难免疏漏,多多理解,欢迎斧正。

Share on: