TypeScript 编译器选项要求在重载声明中添加最终的"catch-all"情况,

vnzz0bqm  于 7个月前  发布在  TypeScript
关注(0)|答案(6)|浏览(75)

🔍 Search Terms

最宽松的捕获所有情况,覆盖,重载,间隙,签名调用错误,要求重载捕获所有情况
这个提案与 #57004 重叠,但不包括从单一匹配重载到多重匹配重载的更改。这使得它的变化小得多。值得注意的是,这个提案满足了可行性检查清单,而57004没有。

✅ Viability Checklist

⭐ Suggestion

在当前的TypeScript中,当编译器面对一个不包含“最宽松的捕获所有情况”的重载时,编译器假设用户通过省略声明了“最宽松的捕获所有情况”可能导致意外错误,因此会发出编译错误以防止这种情况发生。(见下面的示例)。
注意:在这里,“最宽松的捕获所有情况”被称为覆盖)。
这个提案添加了一个新的编译器标志:

--requireOverloadCatchAll

当此标志被设置时,编译器将检查覆盖是否像重载声明那样被声明,或者在用户的源代码中作为重载类型声明,而不是在声明文件(即导入)中编写的重载。
为了向后兼容,该标志的默认值为 false

📃 动机示例

示例1,不必要的签名调用错误
  • 示例1:当前行为-不必要的签名调用错误的简单例子*
function stringOrNum(x: string): number;
function stringOrNum(x: number): string;
function stringOrNum(x: string|number): string|number {
    return x;
}

stringOrNum(Math.random()<0.5 ? "" : 0); // error
//          ~~~~~~~~~~~~~~~~~~~~~~~~~~~
// No overload matches this call.
//   Overload 1 of 2, '(x: string): number', gave the following error.
//     Argument of type 'string | number' is not assignable to parameter of type 'string'.
//       Type 'number' is not assignable to type 'string'.
//   Overload 2 of 2, '(x: number): string', gave the following error.
//     Argument of type 'string | number' is not assignable to parameter of type 'number'.
//       Type 'string' is not assignable to type 'number'.(2769)

当设置 --requireOverloadCatchAll 标志时,用户需要编写覆盖案例,

function stringOrNum(x: string): number;
function stringOrNum(x: number): string;
function stringOrNum(x: string|number): string|number;
function stringOrNum(x: string|number): string|number {
    return x;
}

错误就不会发出。

示例2:当前行为-通过编写覆盖案例可以轻松解决难以理解的错误

一个简单的Map重载函数的结果在应用于数组Map函数时会产生难以理解的错误:

function f(x:string):1;
function f(x:number):2;
function f(x:string|number): 1|2 {
    if (typeof x==="string") return 1;
    if (typeof x==="number") return 2;
    throw "impossible";
}

当设置 --requireOverloadCatchAll 标志时,用户需要编写覆盖案例,

let arr: number[] | string[] = [];
arr = arr.map(f); // error
//      ~
// Argument of type '{ (x: string): 1; (x: number): 2; }' is not assignable to parameter of type '((value: number, index: number, array: number[]) => 2) & ((value: string, index: number, array: string[]) => 2)'.
//   Type '{ (x: string): 1; (x: number): 2; }' is not assignable to type '(value: string, index: number, array: string[]) => 2'.
//     Type '1' is not assignable to type '2'.(2345)
// function f(x: string): 1 (+1 overload)

错误就不会发生。

示例3:当前行为-不编写覆盖案例对库编写者来说不好的做法
  • 示例3a:*

一个简单的Map函数。库编写者在文档中声明了覆盖案例,但没有在实现中声明。

// Library writer
/**
* @throws "rangeError" if (x,y) is not in the input range described
* by the overload declaration.
*/
function g0(x:string,y:number):1;
function g0(x:number,y:boolean):2;
function g0(x:string|number,y:number|boolean):1|2 {
    if (typeof x === "string" && typeof y === "number") return 1;
    if (typeof x === "number" && typeof y === "boolean") return 2;
    throw "rangeError"; // from library writer's POV, not  an unexpected error
}

库客户端收到了一个难以理解的签名调用错误消息,如下所示:

  • 示例3b:*
declare const arr2: [string, number][]|[number, boolean][];
let mappped = arr2.map(g0);
// error               ~~~
// Argument of type '{ (x: string, y: number): 1; (x: number, y: boolean): 2; }' is not assignable to parameter of type '((value: [string, number], index: number, array: [string, number][]) => 2) & ((value: [number, boolean], index: number, array: [number, boolean][]) => 2)'.
//   Type '{ (x: string, y: number): 1; (x: number, y: boolean): 2; }' is not assignable to type '(value: [string, number], index: number, array: [string, number][]) => 2'.
//     Types of parameters 'x' and 'value' are incompatible.
//       Type '[string, number]' is not assignable to type 'string'.(2345)

错误对库客户端来说很麻烦,因为库用户已经在文档中声明了覆盖案例,所以没有出错的风险。
当设置 --requireOverloadCatchAll 标志时,库编写者将被要求编写覆盖案例,

declare function g0(x:string,y:number):1;
declare function g0(x:number,y:boolean):2;
declare function g0(x:number|string,y:boolean):1|2; // throws "rangeError" (library client needs to know this)

客户端就不会遇到这个错误。
反驳的一个论点是,库编写者可能想节省一些字符,不写覆盖案例就写出实现,例如:

function g(x:string|number,y:number|boolean):1|2 {
    if (typeof x === "string" && typeof y === "number") return 1;
    else return 2;
}

然后让编译器发出错误,即使对于用户来说有点麻烦也没关系。这是可能的,但对于库编写者来说并不是一种好的做法,所以这不是一个很好的论据。

示例4:不编写覆盖案例使继承变得困难,对库编写者来说也不是一种好的做法

改编自 issue #56829
当设置 --requireOverloadCatchAll 标志时,库编写者将被要求编写覆盖案例,
这将使库客户端成功继承。

SetupOverload(
    functionSymbol: TypeScript Symbol // e.g. overloadFunction,
    // the extra type in addition to the explicit overloads cover type
    gapReturnType: throws | never | any = never,
    thrownType: any = undefined
): void;

其中

  • functionSymbol 是重载函数声明的符号,
  • gapReturnType 是一个类型,可以是 throwsnever 或其他任何类型,
  • 这里 throws 是一个关键字,但不是一个新的类型。具体来说:throws 对应于 never,而 void 对应于 undefined
  • gapReturnType 被添加到显式重载的覆盖中。
  • thrownType 表示在 gapReturnTypethrows 时应该抛出的类型,否则忽略。

而不是像这样写出最后一个情况的覆盖类型:

function g2(x:string,y:number):1;
function g2(x:number,y:boolean):2;
function g2(x:string|number,y:number|boolean):1|2|throws "rangeError"
function g2(... // implementation

库编写者可以写成:

function g2(x:string,y:number):1;
function g2(x:number,y:boolean):2;
SetupOverload(g2, throws "rangeError");
function g2(... // implementation

同样地,为了定义一个没有声明的重载类型:

type CreateOverload<
    TupleOfFuncs extends [... ((...args:any[])=>any)[]],
    GapReturnType extends throws | never | any = never,
    ThrownType extends any = undefined
>; // intrinsic

SetupOverloadCreateOverload 也包含在提案 #57004 中。尽管 --requireOverloadCatchAll 提案不包括多个匹配,但这些内置函数对于当前的单重载匹配算法也很有用。

更新 1/26/2024 - 为 GapReturn 提供另一个选择:compilerError,它对应于当前不包含 catch-all 情况的行为。考虑以下场景:

  • 用户有一个单独的 UI 行,对应于每个重载情况,因此他们 打算总是使用缩小到单个重载的参数调用 - 他们只是想确保在编译时,UI-to-overload Map是正确的。已知重载函数永远不会以其他方式被调用。
js5cn81o

js5cn81o1#

你能用一个例子来澄清一下你所说的 the implementation signature of a set of overloads 是什么意思吗?我的理解是,重载是实现的签名。
也许问题出在我的说法上。
带有 --requireOverloadsCatchAll 设置时,用户被要求(1)正确处理他们的实现捕获所有情况(没有太多“额外”的工作),并将该情况添加到声明中,以便(3)他们和他们的客户端不会遇到某种编译器错误。
其中 (1) 和 (2) 应该互换。应该是:

  • (1) 提醒用户将捕获所有情况添加到声明中,
  • (2) 这样鼓励用户确保捕获所有情况得到准确实现

当然,理想情况下,编译器尽可能进行类型检查,以确保实现和重载签名正确对应。至少在有限的情况下,有一项开放的问题就是这样做。当然,如果可能的话,也应该检查捕获所有情况。(正在寻找它...)。
但是这个提案并不包括将实现与重载进行类型检查。简单是因为需要保持提案简单。

nfg76nw0

nfg76nw02#

"实现签名"在TypeScript重载中通常指的是最后一个对调用者不可见的签名,包含函数的实现。如果我正确理解了你的提案,你提议引入一个新的标志,强制用户将该签名公开为“覆盖案例”,但默认情况下它没有被公开的部分原因是,通常情况下,对于没有实际缩小范围方法的调用者来说,它过于宽松。
例如,假设我编写了以下代码:

function add(x: number, y: number): number;
function add(x: string, y: string): string;
function add(x: string | number, y: string | number): string | number {
    return x + y;
}

我真的、真的不想让调用者执行 add("foo", 42) 操作,但是启用这个标志将迫使我允许它。

zbdgwd5y

zbdgwd5y3#

如果没有使用通配符:

// lib
function add(x: number, y: number): number;
function add(x: string, y: string): string;
function add(x: string | number, y: string | number): string | number {
    return x as any + y;  // to suppress error, lib writer has the privilege
}

// client
declare const x: string | number;
declare const y: string | number;
// add(x,y); // error
if (typeof x === "string" && typeof y ==="string"){
    add(x,y); // string
} else if (typeof x === "number" && typeof y ==="number") {
    add(x,y); // number
} else {
}

如果你编写的库包含了提案+额外的提案1和2:

/**
 * Throws "rangeError" if typeof x !== typeof y
 */
function add(x: number, y: number): number;
function add(x: string, y: string): string;
function add(....): ... {
    if (typeof x === typeof y)  return x as any + y;
    throw "rangeError";
}
SetupOverload(add, throws "rangeError")

客户端可以编写

declare const x: string | number;
declare const y: string | number;
try { 
    add(x,y); // string | number
}
catch (e) {
}
50few1ms

50few1ms4#

关于这个问题,我能看到的一个问题是:即使它是可选的,基本上所有的库作者都必须采用它,即使它不能准确地模拟他们的API,因为无法确定终端用户是否会为其项目启用它。
TypeScript中没有模块级编译器选项的概念。

krugob8w

krugob8w5#

来自建议部分
当设置此标志时,编译器将检查覆盖是否为用户源代码中的重载声明或重载类型声明声明(即导入的)声明文件中编写的重载声明/类型。我认为这是可能的。
--requireOverloadsCatchAll 不会抑制当重载没有覆盖时的现有编译错误(对于函数的交集也是如此,当它们不包含覆盖时——这是另一个问题)。
只是在使用 --requireOverloadsCatchAll 设置时,用户会被提示(1)正确处理他们的实现捕获所有情况(没有太多“额外”的工作),并将该情况添加到声明中,以便(3)他们和他们的客户端不会遇到某种类型的编译器错误。
即使它不能准确地模拟他们的API
所以他们应该仍然没问题,因为他们的预期编译错误仍然会在他们的客户端代码中触发。

9rygscc1

9rygscc16#

啊,我错过了那部分内容,它不适用于.d.ts文件,谢谢。这实际上是有道理的,因为除非编译器看到实现,否则它甚至不知道覆盖案例应该是什么样子。
现在仍然存在一个问题,即所需的实现签名可能实际上太宽了,以至于库编写者不想暴露它。我想知道在实践中,一组重载的实现签名是用户愿意公开的内容的频率有多高......

相关问题