
在 TypeScript 中,当处理函数交叉类型时,其行为等同于函数重载。然而,在实际调用这类函数时,TypeScript 会根据参数匹配度选择最合适的(通常是第一个)签名来确定返回类型;而在使用 `infer` 进行类型推断时,它却倾向于从最后一个函数签名进行推断,这导致了返回类型的不一致。本文将深入探讨这一现象,并提供重构建议,以确保类型推断的准确性和一致性。
在 TypeScript 中,函数交叉类型(Intersection Types for Functions)的行为与函数重载(Function Overloads)非常相似。一个由多个函数类型通过 & 符号连接而成的交叉类型,会被 TypeScript 解释为一个具有多个调用签名(Call Signatures)的重载函数。
考虑以下示例:
type Foo = (() => Promise<string>) & (() => Promise<any>); type Foo2 = (() => Promise<any>) & (() => Promise<string>);
在这里,Foo 和 Foo2 都被视为具有两个调用签名的重载函数。关键在于,TypeScript 处理重载函数时,在调用和类型推断方面存在不同的行为模式。
当您实际调用一个重载函数时,TypeScript 会根据传入的参数类型,选择“最合适”的调用签名来确定函数的返回类型。在没有参数或参数类型相同的情况下,通常会选择第一个匹配的签名。
例如:
// 示例1: Foo 类型
type Foo = (() => Promise<string>) & (() => Promise<any>);
const a: Foo = async () => {
return "";
};
const b = await a();
// 实际调用时,b 的类型被推断为 string
// 这是因为 (() => Promise<string>) 是第一个签名,且匹配成功。
// ^? const b: string在 Foo 类型中,(() => Promise<string>) 是第一个签名。因此,当 a 被调用时,其返回类型被解析为 Promise<string>,解包后 b 的类型是 string。
// 示例2: Foo2 类型
type Foo2 = (() => Promise<any>) & (() => Promise<string>);
const c: Foo2 = async () => {
return "";
};
const d = await c();
// 实际调用时,d 的类型被推断为 any
// 这是因为 (() => Promise<any>) 是第一个签名,且匹配成功。
// ^? const d: any类似地,在 Foo2 类型中,(() => Promise<any>) 是第一个签名。因此,当 c 被调用时,其返回类型被解析为 Promise<any>,解包后 d 的类型是 any。
与调用行为不同,当您尝试使用条件类型中的 infer 关键字(例如通过 ReturnType 工具类型)从一个重载函数类型中提取返回类型时,TypeScript 通常会从最后一个调用签名进行推断。这是一个已知的 TypeScript 设计限制。
继续上面的示例:
// 示例1: Foo 类型 type Foo = (() => Promise<string>) & (() => Promise<any>); type FooResult = Foo extends () => Promise<infer T> ? T : null; // 使用 infer 时,FooResult 的类型被推断为 any // 这是因为 (() => Promise<any>) 是 Foo 类型中的最后一个签名。 // ^? type FooResult = any
这里,FooResult 被推断为 any,与 b 的实际类型 string 产生了不匹配。
// 示例2: Foo2 类型 type Foo2 = (() => Promise<any>) & (() => Promise<string>); type FooResult2 = Foo2 extends () => Promise<infer T> ? T : null; // 使用 infer 时,FooResult2 的类型被推断为 string // 这是因为 (() => Promise<string>) 是 Foo2 类型中的最后一个签名。 // ^? type FooResult2 = string
同样,FooResult2 被推断为 string,与 d 的实际类型 any 产生了不匹配。
这种调用时取第一个签名、推断时取最后一个签名的行为,正是导致类型不一致的根本原因。
为了避免这种不一致性,并确保类型推断的准确性,最佳实践是尽量避免使用参数类型完全相同的函数交叉类型来模拟重载。如果您的函数没有不同的参数签名来区分不同的重载,那么它可能不适合作为重载函数处理。
如果您的目标是获得一个明确的返回类型,并且函数没有真正的重载逻辑(即不同的参数导致不同的行为),那么应该直接使用一个单一的函数签名来表示其类型。
例如,如果您期望函数始终返回 Promise<string>,则应明确定义:
type MyFunctionType = () => Promise<string>;
const myFunc: MyFunctionType = async () => {
return "hello";
};
type MyFuncResult = MyFunctionType extends () => Promise<infer T> ? T : null;
// ^? type MyFuncResult = string
const result = await myFunc();
// ^? const result: string这样,MyFuncResult 和 result 的类型将保持一致。
如果您确实希望函数的返回类型是多个类型的交叉,那么不应该通过交叉函数类型来实现,而是直接在单一的函数签名中定义一个交叉的返回类型。
// 错误的示例:试图通过函数交叉类型获得交叉返回类型
type BadAttempt = (() => { a: string }) & (() => { b: number });
type BadRet = ReturnType<BadAttempt>;
// ^? type BadRet = { b: number; } // 仅推断出最后一个签名
const badCall = ((): BadAttempt => {
return { a: "", b: 1 };
})();
// ^? const badCall: { a: string; } // 实际调用得到第一个签名
// 正确的示例:直接在单一函数签名中定义交叉返回类型
type CorrectFunction = () => { a: string } & { b: number };
const correctFunc: CorrectFunction = () => {
return { a: "", b: 1 };
};
type CorrectRet = ReturnType<typeof correctFunc>;
// ^? type CorrectRet = { a: string; } & { b: number; }
const correctCall = correctFunc();
// ^? const correctCall: { a: string; } & { b: number; }通过这种方式,CorrectRet 和 correctCall 的类型将完全一致,都反映了 string 和 number 的交叉类型。
通过遵循这些原则,您可以有效地管理 TypeScript 中的函数类型,避免由重载和 infer 行为差异引起的潜在类型问题,从而编写出更健壮、更易于理解的代码。
以上就是TypeScript 函数交叉类型与返回类型推断:深入理解与解决方案的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号