防抖(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(最后一次延迟执行)选项。
interface DebounceOptions {
leading?: boolean;
trailing?: boolean;
}
interface DebouncedFunction<T extends (...args: any[]) => any> {
(...args: Parameters<T>): ReturnType<T> | undefined;
cancel: () => void;
flush: () => ReturnType<T> | undefined;
}
function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number,
options: DebounceOptions = {}
): DebouncedFunction<T> {
const { leading = false, trailing = true } = options;
let timer: ReturnType<typeof setTimeout> | null = null;
let lastResult: ReturnType<T> | undefined;
let lastArgs: Parameters<T> | null = null;
let invoked = false;
const debounced = function (
this: ThisParameterType<T>,
...args: Parameters<T>
): ReturnType<T> | undefined {
const now = !timer;
if (now && leading) {
lastResult = fn.apply(this, args);
invoked = true;
}
if (timer) clearTimeout(timer);
lastArgs = args;
timer = setTimeout(() => {
if (trailing && !(leading && invoked && !timer)) {
lastResult = fn.apply(this, lastArgs!);
}
timer = null;
lastArgs = null;
invoked = false;
}, delay);
return lastResult;
} as DebouncedFunction<T>;
debounced.cancel = () => {
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
lastArgs = null;
invoked = false;
};
debounced.flush = () => {
if (timer !== null && lastArgs) {
clearTimeout(timer);
timer = null;
lastResult = fn.apply(this as any, lastArgs);
lastArgs = null;
invoked = false;
return lastResult;
}
return undefined;
};
return debounced;
}类型标注的收益在这里充分体现:
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 |
泛型约束为函数 |
| 接口 + 交叉类型 | 描述带附属方法的函数类型 |
给防抖函数加类型标注的核心思路:
- 用 泛型
T捕获原始函数的完整签名 - 用
Parameters<T>和ReturnType<T>复用类型信息 - 用 接口 描述返回的函数对象(包含
cancel、flush等方法) - 必要时用
as断言 处理交叉类型的构造过程
这样写出来的防抖函数,调用方能获得完整的类型推断——参数校验、自动补全、文档提示全覆盖,再也不用担心防抖吃掉类型信息了。