TypeScript Multimatch Overloads和一个新的用户合约用于重载声明,

wgeznvg7  于 6个月前  发布在  TypeScript
关注(0)|答案(4)|浏览(43)

🔍 Search Terms

✅ Viability Checklist

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • Maybe not breaking, because it allows more matches, not fewer. But to be safe, call it breaking.
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
  • This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

⭐ Suggestion

  • Note: the terms cover, convex, concave, and gap are defined in the Definitions section at the end of this proposal.
Existing overload matching algorithm - Single Match

The current overload matching algorithm selects a single overload, or fails, as follows:

  • Definition: exact match
  • for every input argument type, that type is assignable to its corresponding overload parameter type (exact match)
  • pseudo-code:*
match = undefined;
for each overload in overload sequence
    if exact match
        matches = overload;
        break
if (!match) compilerError();
Proposed overload matching algorithm (defective version) - Multiple Matches

First, a defective version of the proposed algorithm is described here. Further down, a better version is described.
The (defective) proposed overload matching algorithm selects (possibly multiple) overloads as follows:

  • Definition: partial match
  • for every input argument type, some subset of that type is assignable to its corresponding overload parameter type.
  • Note: an exact match is also a partial match.
  • pseudo-code:*
matches = []
for each overload in overload sequence
    if partial match
        matches.push(overload)
        if exact match
            break
if (matches.length===0) compilerError();
  • The result is a set of overload matches, which can be non-empty even if there is no exact match.
  1. The overload matches have two uses:
  2. To determine the return type of the function call.
  3. To maintain a mapping from return type to parameter types for usage in flow analysis after the call. (Described Later.)
What are the consequences of this proposed change?

This table shows relations between the input argument range and compile errors, for the current (Single Match) and proposed (Multiple Matches) overload matching algorithms, without any other changes. This is for the general case where last overload might not be the cover.

  • Table of Compiler Errors for Input Argument Range vs Algorithm (with Defective Proposed Algorithm)
Input Argument RangeSingle Match AlgoMulti Match Algo
1. Exact Match ExistsNo ErrorNo Error
2. No Partial Match, Within CoverErrorError
3. Partial Match only, Within CoverErrorNo Error (*)
4. Any Part exceeds CoverErrorSometimes No Error (*)

First and foremost, the current (Single Match) algorithm is designed specifically so that any input overlapping the (non-empty) Gap of the overload sequence will fail to match any overload, and will trigger a compile error. This is a designed feature, not a bug. The idea is that no unhandled input can slip through resulting in unpredictable behavior. That's the current contract between the user and TypeScript, and it is a good contract for the user.
In contrast, in the proposed matching algorithm described so far, any input overlapping (case 3), or exceeding (case 4), the Gap of the overload sequence might not trigger a compile error. Unless other changes are made, the proposed algorithm will be a bad contract for the user. The next section describes changes to make it a good contract again.

Contract with User to Handle the Gap Case (User Runtime Contract)
Interface and Usage

Suppose an overload function declaration

function overloadFunction(...): ...; // overload 1
function overloadFunction(...): ...; // overload 2
function overloadFunction(a,b,c) { // implementation
    // ...
}

An new intrinsic function is proposed:

SetupOverload(
    functionSymbol: TypeScript Symbol // e.g. overloadFunction,
    gapReturnType: throws | never | any = never,
    thrownType: any = undefined
): void;

where

  • functionSymbol is the symbol for the overload function declaration,
  • gapReturnType is a type that is either throws , never , or any other type,
  • Here throws a keyword but not a new type. To be specific: throws is to never as void is to undefined .
  • thrownType indicated the type that should be thrown when gapReturnType is throws , otherwise ignored.

重载类型将为TypeScript内部使用提供以下公共接口:

  • getParametersCover() :
  • explicitOverloads 中提供的 SetupOverload 的所有参数的覆盖范围
  • 用于推理
  • 这也将是 type 函数 Parameters<typeof overloadFunction> 的返回值
  • getReturnCover() :
  • SetupOverload 中提供的 ReturnType<typeof overloadFunction> 的所有参数的联合返回类型
  • 用于推理
  • 这也将是 type 函数 ReturnType<typeof overloadFunction> 的返回值
  • getExplcitOverloads() :
  • SetupOverload 中提供的 overloadFunction 的重载
  • 在匹配和流分析/类型检查 getGapReturnType() 实现期间使用。
  • SetupOverload :
  • overloadFunction 所提供
  • 在匹配和流分析/类型检查 getThrownType() 实现期间使用。
  • SetupOverload :
  • overloadFunction 所提供
  • 在匹配和流分析/类型检查 SetupOverload 实现期间使用。

overloadFunctionCoverReturnType 执行之前需要由编译器在 throws 实现进行流分析/类型检查之前执行,其中将用于检查返回类型(尽可能多)。
throws 和它们的含义如下:

  • never :这是一个新关键字。当用户指定 never 时,用户同意通过抛出异常来处理运行时的“Gap”情况。
  • gapReturnType :当用户指定 throws 时,他们同意两种情况之一:
  • “Gap”情况为空,或者
  • “Gap”情况不为空,但用户控制输入参数范围并知道输入永远不会与“Gap”重叠。
  • 其他任何类型:当用户指定任何其他类型时,他们同意在运行时通过返回该类型的值来处理“Gap”情况。

throws 应作为重载类型的部分显示,以便客户端可以看到用户的合同。
关于 throws 的注意事项:

  • 即使函数异常尚未在 TypeScript 流分析中实现,函数返回值 throws 对于用户和客户端来说仍然是一个有意义的合同。换句话说,实现函数 CoverReturnType 的流分析不是使用 throws 作为 throws 值的先决条件。实际上,该关键字也可以用作显式定义的任何显式重载的返回值,原因相同。
多重匹配算法(非缺陷版本)

完整的、非缺陷的、提议的多重匹配算法如下:

  • 伪代码:*
matches = []
hasExactMatch = false
for each explicit overload in overload sequence
    if partial match
        if (overload return type !== throws and overload return type !== never)
            matches.push(overload)
        if exact match
            hasExactMatch = true
            break
if (!hasExactMatch)
    if not exact match for implicit cover overload
        compilerError();
    else if (CoverReturnType !== throws and CoverReturnType !== never)
        matches.push(implicit cover overload)
if (matches.length===0) compilerError();

更新后的“Table of Compiler Errors”如下:

  • Table of Compiler Errors for Input Argument Range vs Algorithm*
Input Argument RangeSingle Match AlgoMulti Match Algo
精确匹配存在没有错误没有错误
没有部分匹配且在覆盖范围内有错误有错误
只有部分匹配且在覆盖范围内有错误User Runtime Contract
任一部分超过覆盖范围有错误有错误

现在差异缩小到第3种情况。使用“User Runtime Contract”是一种权衡,将在下面的示例部分进行审查。

隐式最终重载

从概念上讲,重载类型除了显式重载之外还有一个隐式的最终重载。因为这是在显式重载之后匹配到的最后一个重载,所以隐式重载具有显式重载参数序列的“Gap”的隐式参数类型,因为这是在匹配到显式重载之后剩下的唯一内容。
隐式重载的返回类型是 gapReturnType。隐式重载的参数是显式重载参数的“覆盖”。但是返回类型只有 gapReturnType

创建没有函数声明的重载类型

还需要能够创建没有引用函数声明的重载。这可以通过以下内置函数实现:

type CreateOverload<
    TupleOfFuncs extends [... ((...args:any[])=>any)[]],
    GapReturnType extends throws | never | any = never,
    ThrownType extends any = undefined
>; // intrinsic
// usage
const overloadFunction: CreateOverload<....> = (a,b,c) => { /* implementation */ }

#####动机示例#####示例 1:嵌套循环中的重载基于从 @RyanCavanaugh借用并扩展的案例的研究。根据提案-

示例3:捕获输入与返回类型之间的关联

对于具有简单返回类型的一些重载函数,反向函数Map允许类似于const变量在流分析中具有的关联功能 - 它们可以捕获多个变量之间的关联。

declare const a:1|2|3;
declare const b:1|2|3;
declare const c:1|2|3;

const correlationCapture: CreateOverload<
    [
        (a: 1, b: 1, c: 1) => "a",
        (a: 2, b: 2, c: 1|2) => "b",
        (a: 3, b: 3, c: 1|2|3) => "c"
    ],
    throws
> = (a,b,c) => {
    if (a===1 && b===1 && c===1) return "a";
    if (a===2 && b===2 && (c===1 || c===2)) return "b";
    if (a===3 && b===3 && (c===1 || c===2 || c===3)) return "c";
    throw undefined;
}

const r = correlationCapture(a,b,c); // "a"|"b"|"c"|throws undefined
// demultiplexing
if (r==="a") {
    // a:1, b:1, c:1
} else if (r==="b") {
    // a:2, b:2, c:1|2
} else if (r==="c") {
    // a:3, b:3, c:1|2|3
}

💻 用例

  1. 如同示例1所示,有些情况下单精确匹配的重载函数无法有效地捕获类型的有效组合范围。部分匹配解决了这个问题。
  2. 在许多情况下,提供间隙重载参数和最终覆盖是很繁琐的,因为这需要准确地编写一个复杂的类型,这就是为什么它经常被跳过,如同示例2所示。使用setupOverload的优点是
  • 与手动编写覆盖相比,使用起来更加容易
  • 用户被迫考虑间隙情况,并就如何处理它做出决策 - 这是合同。
  • 拥有正确的覆盖包括间隙类型可以使编译器更容易进行流分析和推断(将在另一个提案中讨论)。
  1. 如示例3所示,反向函数Map允许类似于const变量在流分析中具有的关联功能 - 它们可以捕获多个变量之间的关联。在某些情况下,这可能是有用的。

需要更多细节

实现新的重载类型,使其与旧的重载类型共存

以避免破坏性更改。

对提议的重载的推断、联合和交集的影响

需要详细说明提议的重载的推断、联合和交集的影响。

定义

重载的覆盖定义

假设为函数 f 定义了一系列重载如下:

declare function f(...args: P1):R1
declare function f(...args: P2):R2
...
declare function f(...args: PN):RN

该序列的覆盖是一个唯一的类型,该类型可以按以下方式定义规范:

type MyCoverOfF = (...args: P1 | P2 | ... | PN): R1 | R2 | ... | RN;

注意覆盖是通过在每个参数(和返回类型)上取类型之并来形成的。

  • 伪代码:*
Parameters<MyCoverOfF>[n] === Union over all nth parameters of the overloads
ReturnType<MyCoverOfF> === Union over all return types of the overloads
凸/凹重载与间隙的定义
  • 一个序列是的,如果且仅当其覆盖所允许的每个输入都被重载匹配。
  • 如果不是凹的,那么它是的。
  • 等效地,一个序列是的,如果且仅当存在一些由覆盖允许但不能被任何重载匹配的输入。
  • 由覆盖允许但不能被任何重载匹配的输入集合在这里定义为序列的间隙。
  • 当然,重载和间隙的组合空间就是覆盖。

“凹”和“凸”是从集合理论和几何学借用的概念,在这里参数和返回类型是空间轴。
“间隙”则是在这个解释中仅简要定义的一个术语。
这里有一个凸序列重载的例子(来自TypeScript文档):

declare function makeDate(timestamp: number): Date;
declare function makeDate(m: number, d: number, y: number): Date;

上述序列的覆盖类型的唯一性。可以这样定义:

(mOrTimestamp: number, d?: number, y?: number) => Date;

这相当于上面定义的规范:

(...args: [number]|[number, number, number]) => Date

上面的输入 ...[1,1] 位于上述序列的间隙中。

makeData(1,1) // error

一个更一般的例子:

declare const a:number;
declare const b:number|undefined;
declare const c:number|undefined;
makeDate(a,b,c) // error on parameter 'b'
//         ~
// Argument of type 'number | undefined' is not assignable to parameter of type 'number'.
//   Type 'undefined' is not assignable to type 'number'.(2345)
// const b: number | undefined

在这两种情况下,TypeScript编译器都会友好地通知我们输入重叠了序列的间隙。

1aaf6o9v

1aaf6o9v2#

"凸"实际上是一个非常有用的术语。

i34xakig

i34xakig3#

一个重载序列是凸**的,当且仅当它的覆盖范围中的每一个可能允许的输入也与这些重载匹配。

一个重载序列是凸**的,当且仅当存在一些无法被任何重载匹配的输入。

这两个说法似乎有矛盾。在第三个要点中,你是否打算写?否则,我只能将这个语句组合解释为“接受所有输入(即 ...args: any[] )=凹”,这似乎不是一个有用的定义。

jvlzgdj9

jvlzgdj94#

这是错误的,而且它没有明确指出 convex 在这里是指相对于覆盖物的凸函数。

  • 如果一个函数序列是凹的,那么它的每个输入都必须与覆盖物允许的输入相匹配。
  • 如果不是凹的,那么它是凸的。
  • 等价地,如果一个函数序列是凸的,那么存在一些覆盖物允许的输入无法被任何函数匹配。

实际上,在多匹配中,基参数可以组合,这些组合所覆盖的空间比基向量本身的空间更大 - 因此联合类型可以获得正确的输入输出Map返回值。这个覆盖的空间本身就是一种“凹”函数,但仍然可以相对于“覆盖物”来说是“凸”的。我需要将这一点添加到文档中。

相关问题