TypeScript 函数交集的返回类型不完整且取决于声明顺序-一种修复它的算法,

bpzcxfmw  于 3个月前  发布在  TypeScript
关注(0)|答案(9)|浏览(39)

🔍 搜索词

#57002
#41874
函数交集,返回类型,不完整,声明顺序依赖

✅ 可可行性检查清单

⭐ 建议

函数交集的返回类型至少应该是完整的,并且独立于函数声明的顺序。

📃 激励示例

  • 示例1:非重载函数的交集
interface A0 {
    foo(): string;
}

interface B0 {
    foo(): number;
}

declare const ab0: A0 & B0;
declare const ba0: A0 & B0;

const rab = ab0.foo(); // actual string, expecting string | number
const rba = ba0.foo(); // actual number, expecting string | number
  • 示例2:重载函数的交集
interface A1 {
    bar(x:1): 1;
    bar(x:2): 2;
}
interface B1 {
    bar(x:2): "2";
    bar(x:3): "3";
}

declare const ab1: A1 & B1;

const rab11 = ab1.bar(1); // actual 1, expecting 1
const rab12 = ab1.bar(2); // actual 2, expecting 2 | "2"
const rab13 = ab1.bar(3); // actual 3, expecting 3

declare const ba1: B1 & A1;

const rba11 = ba1.bar(1); // actual 1, expecting 1
const rba12 = ba1.bar(2); // actual "2:, expecting 2 | "2"
const rba13 = ba1.bar(3); // actual 3, expecting 3
  • 示例3:具有对象返回类型的函数的交集
interface A2 {
    bar(): {a: string};
}
interface B2 {
    bar(): {b: string};
}

declare const ab2: A2 & B2;
declare const ba2: B2 & A2;

const rab21 = ab2.bar(); // actual: {a: string}, expecting: {a: string} | {b: string}
const rba21 = ba2.bar(); // actual: {b: string}, expecting: {a: string} | {b: string}
当前算法:
  • g = g[0] & g[1] & ... 当作有序重载函数 { g[0] ; g[1] ; ....} 来处理。
  • args 成为参数。
  • return ReturnType<chooseOverload(g, args)>

当前算法将 g 完全视为有序重载函数 { g[0] ; g[1] ; ....}。因此,args 可以匹配最多一个交集成员 g[i] ,从而导致不完整且依赖于声明顺序的返回类型。

提议算法 # 1:
  • g = g[0] & g[1] & ... 成为函数的交集。
  • args 成为参数。
  • returnType = never //初始化
  • 对于 g[i]g
  • 如果 g[i] 是重载函数
  • returnType = returnType | ReturnType<chooseOverload(g[i], args)>
  • 否则如果 args 扩展自 Parameters<g[i]>
  • returnType = returnType | ReturnType<g[i]>
  • return returnType

这对于上述示例应该有效。

提议算法 # 2:

与当前算法相比,提议算法 # 1是一个更昂贵的计算,但它是完整的,并且不依赖于声明顺序。如果那个计算太昂贵了,那么可以使用一个更简单的算法:

  • 返回类型是 iReturnType<g[i]> 的并集。如果 g[i] 是重载函数,则其计算方式为该重载序列的 catch-all(也称为覆盖)情况的返回类型。

理由:

  • cover(g) 成为 catch-all(也称为覆盖)情况(即重载序列中的最后一个)。也就是说,对于每个参数索引 paramIndex , Parameters<cover(g)>[paramIndex] 是 x1m26n
iecba09b

iecba09b1#

这听起来像是 #57089 的重复问题。为什么要打开另一个问题?你可以直接编辑并打开你的另一个问题。 🤷‍♂️
const rab = ab0.foo(); // 实际字符串,期望字符串 | 数字
这仍然对我来说没有意义。你有一个总是返回 string 的函数,另一个总是返回一个 number 的函数。然后你说你们有它们的交集,这意味着返回类型也必须是交集,而不是联合。所以它应该是 string & number 而不是 string | number

nbysray5

nbysray52#

老实说,我在这里看不到价值。提议的算法在显式编写的重载和由交集形成的重载之间引入了不一致性,并破坏了人们肯定编写的实际代码,其中较早的重载返回比它遮蔽的更具体的类型。我们得到了什么回报?这似乎像是用一个问题换取另一个问题。

wwodge7n

wwodge7n3#

MartinJohns

然后你说它们有交集,这意味着返回类型也必须是交集,而不是并集。为什么一定要这样呢?如果它是交集,那么

(()=>string) & (()=>number)

的返回类型将永远不会是交集。这似乎是通过矛盾证明返回类型不是交集。我不明白我们如何为这个特殊情况开一个例外,所以它必须适用于所有情况。

fsi0uk1n

fsi0uk1n4#

@RyanCavanaugh

提议的算法...破坏了人们编写的实际代码,其中较早的重载返回的类型比它所遮蔽的类型更具体。
例如:

interface A {
    f(x:911):"emergency";    
}
interface B {
    f(x:number):number;    
}
// current
declare const ab: A&B;
ab.f(911);  // "emergency"
declare const ba: B&A;
ba.f(911); // "emergency"

// under proposal
// declare const ab: A&B;
// ab.f(911);  // "emergency" | number
// declare const ba: B&A;
// ba.f(911); // "emergency" | number

说实话,我看不出这里的价值。提议的算法在显式编写的重载和由交集形成的重载之间引入了不一致性,并破坏了人们编写的实际代码,其中较早的重载返回的类型比它所遮蔽的类型更具体。我们得到了什么回报?这似乎像是用一个问题换取另一个问题。
在上面的例子中,提案将返回 "emergency" | number ,它更宽。
而当前的TypeScript对于 A&BB&A 都返回 "emergency" ,但它只适用于 B&A ,因为TypeScript已经将 B&A 视为一个重载,并重新排序了重载的成员,这对复杂重载来说是有代价的 - 在计算时间和复杂性/维护逻辑方面都有代价。
然而,对于独立且无序的重载情况,当前算法选择了一个任意类型,这个类型必须太窄。太窄的类型比太宽的问题要严重得多。

mutmk8jj

mutmk8jj5#

为什么必须这样?
因为这是唯一合乎逻辑的做法。联合类型没有意义且不安全。(()=>string) & (()=>number) 可以分配给 (()=>string) ,因此调用者会期望一个 string 。它也可以分配给 (()=>number) ,因此调用者会期望一个 number 。在某些情况下返回一个 string ,在其他情况下当每个相应类型都说它总是一个 number (即联合类型)时,返回一个 stringnumber 是错误的。
是的,逻辑结果是返回类型最终变为 never ,因为 (()=>string) & (()=>number) 是一种无法在 JavaScript 中实现的类型。你不能有一个函数同时返回一个 string 和一个 number ,但这就是这种类型所说的。

pgx2nnw8

pgx2nnw86#

@MartinJohns - 一个 & 函数表示一组重载序列的成员,但不表示它们的顺序,因此包括所有可能的顺序。无论选择哪种重载顺序,但在调用点(如果有多个匹配项,则为一个或多个)将匹配一个(或多个)重载函数,该重载函数的返回类型将作为输出。

interface Y<T> {
  g(f:()=>T):void
};
declare const yy:Y<string> | Y<number>;
yy.g; // (method) Y<T>.g(f: (() => string) & (() => number)): void

在这里,参数 fg 必须是一个重载函数。f 的返回类型将是 stringnumber,具体取决于未知的重载顺序,因此类型必须是 string|number

当然,这是一个非常奇怪的边缘情况,因为两个重载顺序都有不合理的阴影,所以它永远不会被实现为重载函数,而是作为单个函数。如果算法能处理这样的边缘情况,那么在使用这个建议的情况下就会发生这种情况。

最后,但同样重要的是,在原始帖子 示例1 中,当前的行为不是 never 而是 string

const rab = ab0.foo(); // actual string, expecting string | number

它是 string,因为当参数 {()=>string;()=>number} 传递给 chooseOverload 时,第一个重载函数被返回。

tag5nh1u

tag5nh1u7#

以下是关于当前行为的至少一个证明:

interface A0 {
    foo(): string;
}

interface B0 {
    foo(): number;
}

declare const test2: (A0 & B0) | (B0 & A0);
const r2 = test2.foo();
//       ^? string | number

如果,正如你所Assert的那样,(A0 & B0)["foo"](B0 & A0)["foo"] 都返回了 never ,那么显然 test2.foo() 必须返回 never ,但它却返回了 string|number
我写这篇文章的目的是主张,对于 (A0 & B0)["foo"](B0 & A0)["foo"] 的每一种情况,都应该返回 string|number ,而它们实际上只分别返回了 stringnumber ,这是由于 TypeScript 目前将 & -intersection 错误地当作一个重载连接操作符。

thtygnil

thtygnil8#

老实说,我觉得这里没有什么价值。提议的算法在显式编写的重载和由交集形成的重载之间引入了不一致性,并破坏了人们编写的实际代码,其中较早的重载返回的类型比它遮蔽的类型更具体。我们能得到什么回报呢?这似乎像是用一个问题换来另一个问题。
这些已经不一致(#56951),除非交集以特定顺序排列,否则ReturnType将给出错误的结果:
用户本意应该做的是:

  • 编写他们的重载
  • 最后有一个“通配符”重载,代表函数在重载解析结果不明确时的行为的返回类型应该是所有其他签名返回类型的联合体

除非人们遵循这个规则,否则ReturnType无法以多种方式完成其工作,包括这个。对于像这样完全没有运行时对应物的东西,类型系统对它没有什么有意义的话可说。
我宁愿让交集具有一些与顺序无关的行为,特别是考虑到当前提供的顺序相关行为与函数重载不匹配。

xsuvu9jc

xsuvu9jc9#

我认为这个提案的写法是不可行的。正确返回类型应该是适用签名的返回类型的交集。
也就是说,type F = ((x:X1)=>Y1) & ((x:X2)=>Y2) 应该被推断为:

  • 如果 x 满足 X1X2,则为 Y1 & Y2
  • 如果 x 满足 X1 但不满足 X2,则为 Y1
  • 如果 x 满足 X2 但不满足 X1,则为 Y2
  • 如果 x 满足 X1 | X2,且两者都不是彼此的合适子类型,则为 Y1 | Y2
  • 否则,类型错误。

这意味着您仍然可以指定一个最通用的重载函数,但可以获得更具体重载函数的好处。
示例 1:ab0ba0 都是 never
示例 2:ab1.bar(2)ba1.bar(2) 都是 never
示例 3:rab21rba21 都是 {a:string, b:string}

相关问题