我们面临一个挑战:下面的 retry 函数无法正确推断其返回的 Promise 类型。你能修复它吗?
async function retry(
fn: () => Promise<any>,
retries: number = 5
): Promise<any> {
try {
return await fn();
} catch (err) {
if (retries > 0) {
console.log("Retrying...");
return await retry(fn, retries - 1);
}
throw err;
}
}
const getString = () => Promise.resolve("hello");
const getNumber = () => Promise.resolve(42);
retry(getString).then((str) => {
// str 应该是 string 类型,而不是 any!
console.log(str);
});
retry(getNumber).then((num) => {
// num 应该是 number 类型,而不是 any!
console.log(num);
});
问题根源分析
上述问题的根源在于,我们在 retry 函数中为 Promise 的返回值类型使用了 any:
async function retry(
fn: () => Promise<any>,
retries: number = 5
): Promise<any> {
// ...
}
这导致当我们调用 retry 函数时,TypeScript 无法推断出解析后 Promise 的确切类型,它被笼统地标记为 any。
这带来了显著的问题:any 类型会使其作用域内的所有类型检查失效。这意味着,仅仅因为使用了 retry 这个可复用函数,我们就可能丢失传递给它的函数原本具有的类型安全保障。这是使用 TypeScript 进行开发时一个常见痛点——开发者有时会为了方便而使用 any,但这会牺牲掉类型系统带来的核心优势。
解决方案:引入泛型类型参数
为了使函数既灵活又类型安全,我们可以使用泛型来替代固定的 any 类型。
async function retry<T>(
fn: () => Promise<T>,
retries: number = 5
): Promise<T> {
try {
return await fn();
} catch (err) {
if (retries > 0) {
console.log("Retrying...");
return await retry(fn, retries - 1);
}
throw err;
}
}
我们为 retry 函数添加了一个泛型类型参数 T。这个 T 随后被用于两个方面:
- 约束
fn 参数,表示它必须返回一个 Promise<T>。
- 作为
retry 函数自身的返回类型 Promise<T>。
通过这样的改造,retry 函数就变成了一个泛型函数。它能够从传入的运行时函数中捕获并传递其返回的 Promise 的具体类型信息。
const getString = () => Promise.resolve("hello");
retry(getString).then((str) => {
// str 现在是明确的 string 类型!
console.log(str);
});
现在,TypeScript 能够根据 getString 的返回值类型,正确地推断出调用 retry(getString) 后返回的 Promise 解析值为 string 类型,而不再是模糊的 any。泛型参数的名字可以是任何你喜欢的,常见的命名如 TData 或 TResponse,前缀 T 通常用来表示这是一个类型参数。通过这种方式,我们极大地提升了函数的通用性和使用时的类型安全性,这是构建高质量、可维护的 JavaScript 与 TypeScript 应用的关键一步。
|