对防抖函数进行类型标注

2025-06-21 前端

防抖(debounce)是前端开发中最常用的性能优化手段之一。当我们用 TypeScript 编写或封装防抖函数时,准确的类型标注能让调用方获得完整的类型推断和编辑器智能提示,避免运行时类型错误。

这篇文章从一个基础防抖函数出发,逐步加上完整的类型标注,涵盖泛型参数、this 上下文、返回值类型和工具方法。

一、基础防抖实现(无类型)

先看一个纯粹的 JavaScript 防抖函数:

function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

调用时没有任何类型保护:

const log = debounce((msg) => console.log(msg), 300);
log(123);        // 应该报错但不会
log("hello");    // 可以

问题很明显:fn 的参数类型丢失了,delay 可以是任何值,返回的函数也丢失了签名信息。

二、加上基础类型标注

最直观的方式:把 fn 声明为函数类型,delay 声明为 number。

function debounce(fn: (...args: any[]) => void, delay: number): (...args: any[]) => void {
  let timer: ReturnType<typeof setTimeout> | null = null;
  return function (...args: any[]) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

好了一点,但 any[] 等于放弃了类型检查——调用方传什么都行,返回的函数也丢失了原始参数类型。

三、用泛型锁定参数类型

TypeScript 的 泛型 是解决这类问题的关键。

function debounce<T extends (...args: any[]) => void>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout> | null = null;
  return function (...args: Parameters<T>) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

关键点:

  • T extends (...args: any[]) => void —— 约束 T 必须是函数类型,且返回值是 void(防抖场景一般忽略返回值)
  • Parameters<T> —— 内置工具类型,提取 T 的参数元组类型
  • 返回的函数签名使用 ...args: Parameters<T>,参数类型与原始函数完全一致

现在调用方可以获得完整的类型推断:

const log = debounce((msg: string) => console.log(msg), 300);
log(123);        // ❌ 类型错误:number 不能赋值给 string
log("hello");    // ✅ 正确

四、保留函数的返回值类型

有时防抖函数可能需要返回值(比如用 leading 选项立即执行一次)。这时返回值不能简单写 void。

定义两个类型参数:一个约束函数类型,一个表示返回值。

function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => ReturnType<T> | undefined {
  let timer: ReturnType<typeof setTimeout> | null = null;
  return function (...args: Parameters<T>): ReturnType<T> | undefined {
    clearTimeout(timer);
    const result = fn.apply(this, args);
    timer = setTimeout(() => {}, delay);
    return result;
  };
}

ReturnType<T> 提取 T 的返回值类型。如果 fn 返回 number,防抖后的函数也返回 number | undefined。

注意:标准 trailing 防抖没有返回值,这里仅作示例展示 ReturnType 的用法。

五、处理 this 上下文

上面所有示例都用了 fn.apply(this, args),TypeScript 默认也能正确推断 this。但如果想要更严谨,可以显式标注 this 参数:

function debounce<T extends (this: any, ...args: any[]) => any>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout> | null = null;
  return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
  • ThisParameterType<T> —— 提取 T 中 this 参数的类型
  • 返回函数显式标注 this: ThisParameterType<T>,保障 this 传递时类型不丢失

六、完整版:带 cancel 方法的防抖

实际项目中,防抖函数往往返回一个带 cancel() 方法的对象,方便手动取消防抖。

interface DebouncedFunction<T extends (...args: any[]) => any> {
  (...args: Parameters<T>): void;
  cancel: () => void;
}

function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay: number
): DebouncedFunction<T> {
  let timer: ReturnType<typeof setTimeout> | null = null;

  const debounced = function (this: ThisParameterType<T>, ...args: Parameters<T>) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  } as DebouncedFunction<T>;

  debounced.cancel = () => {
    if (timer !== null) {
      clearTimeout(timer);
      timer = null;
    }
  };

  return debounced;
}

使用示例:

const save = debounce((id: number) => {
  fetch("/api/save", { method: "POST", body: JSON.stringify({ id }) });
}, 500);

save(42);       // ✅ 参数类型正确
save.cancel();  // ✅ cancel 方法可调用

七、带 leading/ trailing 选项的完全版

更完善的防抖函数支持 leading(首次立即执行)和 trailing(最后一次延迟执行)选项。

类型标注的收益在这里充分体现:

const search = debounce(
  (query: string, page: number) => {
    return fetch(`/api/search?q=${query}&page=${page}`).then(res => res.json());
  },
  300,
  { leading: true }
);

// search 的类型自动推断为:
// (query: string, page: number) => Promise<unknown> | undefined
search.cancel();  // ✅
search.flush();   // ✅

八、总结

类型工具 作用
Parameters<T> 提取函数参数元组
ReturnType<T> 提取函数返回值类型
ThisParameterType<T> 提取 this 参数类型
T extends (...args: any[]) => any 泛型约束为函数
接口 + 交叉类型 描述带附属方法的函数类型

给防抖函数加类型标注的核心思路:

  1. 用 泛型 T 捕获原始函数的完整签名
  2. 用 Parameters<T> 和 ReturnType<T> 复用类型信息
  3. 用 接口 描述返回的函数对象(包含 cancel、flush 等方法)
  4. 必要时用 as 断言 处理交叉类型的构造过程

这样写出来的防抖函数,调用方能获得完整的类型推断——参数校验、自动补全、文档提示全覆盖,再也不用担心防抖吃掉类型信息了。