【问题标题】:Type constraints for overloaded function in TypeScriptTypeScript 中重载函数的类型约束
【发布时间】:2021-05-26 12:12:40
【问题描述】:

所以我可以重载函数:

function myFunc(x : number) : number
function myFunc(x : string) : string
function myFunc(x : number | string) : number | string {
  if (typeof x == "string") {
    return x + "1"
  } else {
    return x + 1
  }
}

它有效:

const x = myFunc(1)   // correctly inferred as number
const y = myFunc("1") // correctly inferred as string

此语法不会防止重载实现中的混合类型:

function myFunc(x : number) : number
function myFunc(x : string) : string
function myFunc(x : number | string) : number | string {
  if (typeof x == "string") {
    return 1 // !!! no type error
  } else {
    return "1" // !!! no type error
  }
}

如果我添加泛型,即使是“正确”版本也会出现错误:

function myFunc(x : number) : number
function myFunc(x : string) : string
function myFunc<T extends number | string>(x : T) : T {
  if (typeof x == "string") {
    return "1" // !!! ERROR
  } else {
    return 1 // !!! ERROR
  }
}

我得到了很好的旧:

TS2322: Type 'number' is not assignable to type 'T'.   'number' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'string | number'.

两个分支。基本上和刚刚的一样

function myFunc<T extends number | string>(x : T) : T {
  if (typeof x == "string") {
    return "1" // !!! could be instantiated with a different subtype... ERROR
  } else {
    return 1 // !!! could be instantiated with a different subtype... ERROR
  }
}

有没有办法限制重载函数中的输入输出类型,使其不具有上述限制?这看起来很常见,但不知何故我在 Google 中找不到答案。

【问题讨论】:

  • return x + 1 // !!! no type error - 即使没有联合类型打字稿也允许这个typescriptlang.org/play?#code/…
  • @Ivan Kleshnin 不要忘记 TS 只是 JS 的扩展。 + 允许对字符串进行操作
  • 谢谢。我更新了描述以关注主要问题(不归结为操作员)。
  • 通用版本的问题在于,从打字稿的角度来看,它可以称为myFunc&lt;'foo'&gt;('foo'),例如

标签: typescript


【解决方案1】:

总结:这是 TypeScript 中一些设计限制或缺失功能的结果。重载是故意不健全的。您可以尝试解决此问题以获得更严格的类型检查,但这很丑陋且不值得(在我看来)。泛型没有帮助,并且有其自身的缺点。一般来说,最方便的做法就是小心实施并继续前进。


无论好坏,重载实现签名被有意允许比所有调用签名的交集更宽松。

大致上,允许实现的返回类型是所有调用签名的返回类型中的union,即使这忽略了任何特定调用签名的输入和输出之间的任何关系。只要myFunc() 的实现接受number | string 类型的参数并返回number | string 类型的值,编译器就很高兴......即使实现在相关调用签名的情况下返回number声明它返回一个string。这是 TypeScript 的类型系统故意未能成为 sound 的地方之一。

microsoft/TypeScript#13235 有一个功能请求,要求严格检查每个调用签名的功能实现。当 TypeScript 团队it was discussed 时,他们确定这样的功能会严重扩展(在调用签名的数量上类似于 n2),人们在重载实现中犯这样的错误太少了值得额外的编译时间。该功能因“过于复杂”而关闭,后来此类请求已被拒绝。

因此编译器不会自动帮助检测不正确的重载实现。


为您提供更多保证的一种可能的解决方法是尝试使用control flow analysis 的结果来检查您的returning 值是否与正确的调用签名相对应。这适用于您的特定示例函数......但它并不总是有效(请参阅后面的泛型部分)。即使它有效,它也丑陋并且有一些不可避免的(但很小的)运行时影响:

function myFunc(x: number): number
function myFunc(x: string): string
function myFunc(x: number | string): number | string {
    if (typeof x == "string") {
        const ret = x + "1";
        let test: typeof ret = (false as true) && myFunc(x) // okay
        return ret;
    } else {
        const ret = x + 1;
        let test: typeof ret = (false as true) && myFunc(x) // okay
        return ret;
    }
}

在这里,我将预期的返回值保存到一个名为ret的变量中,然后强制编译器假装它正在调用myFunc(x)(false as true) &amp;&amp; ...位使编译器认为&amp;&amp;之后的东西实际上正在运行,即使在运行时它不会,这很好,因为我们不想实际做任何事情)。如果编译器很乐意将myFunc(x) 的假定结果分配给类型与ret 相同的变量,那么一切都很好。如果你犯了错误,你会收到警告:

function myFunc(x: number): number
function myFunc(x: string): string
function myFunc(x: number | string): number | string {
    if (typeof x == "string") {
        const ret = 1;
        let test: typeof ret = (false as true) && myFunc(x) // error
        // Type 'string' is not assignable to type '1'.
        return ret;
    } else {
        const ret = "1";
        let test: typeof ret = (false as true) && myFunc(x) // error
        // Type 'number' is not assignable to type '"1"'
        return ret;
    }
}

这样可行,但我个人不会这样做,除非实施错误的后果非常可怕。


至于关于函数的泛型版本的部分......首先,调用签名需要是这样的:

declare function myFunc<T extends number | string>(
  x: T): T extends number ? number : string;

您不能返回T,因为literal types"hello"123 存在,并且您不想声称myFunc(123) 返回123,只是number。但无论如何,即使使用正确的版本,编译器也会给你同样的错误:

function myFunc<T extends number | string>(x: T): T extends number ? number : string {
    if (typeof x == "string") {
        return x + "1"; // error!
        // Type 'string' is not assignable to type 'T extends number ? number : string'.
    } else {
        return x + 1; // error!
    }
}

这是 TypeScript 的另一个缺失的特性;编译器无法验证特定值(如x + "1")是否可分配给依赖于未指定的generic 类型参数的conditional type。当T 尚未解析时,编译器只是推迟评估此类类型,因此T extends number ? number : string 太不透明,编译器无法查看x + "1" 是否是该类型的值。

关于这个的规范问题可能是microsoft/TypeScript#33912,它要求对实现返回类型就是这样一个未解决的条件类型的函数提供一些支持。那里还没有做任何事情,而且,这是一个很难解决的问题。

目前,除非您在每个 return 处使用 type assertions,否则此类函数往往会给出各种编译器警告:

function myFunc<T extends number | string>(x: T): T extends number ? number : string {
    if (typeof x == "string") {
        return x + "1" as any
    } else {
        return x + 1 as any
    }
}

实际上,我通常通过将实现切换为重载来处理这种事情(因此调用签名是通用的,而实现签名不是):

function myFunc<T extends number | string>(x: T): T extends number ? number : string;
function myFunc(x: number | string) {
    if (typeof x == "string") {
        return x + "1";
    } else {
        return x + 1;
    }
}

在这种情况下,可能具有讽刺意味的是,它通过依赖最初引发这个问题的重载实现的不健全性来防止编译器错误。

哦,好吧!


我的建议是小心你的重载实现,说服自己它们是类型安全的,然后继续。这是迄今为止我能想到的最不痛苦的解决方案,尽管它对于那些关心类型安全的人来说并不是很满意。

Playground link to code

【讨论】:

  • 非常感谢您以如此通俗易懂的方式写下答案!我当然从中学到了很多。是否有一些技巧可以缩短具有多个元素(3 个以上分支)的联合的条件?
  • 你需要决定你真正想要的输入/输出关系是什么......也许它只是:给定一组类型U(表示为一个元组以保持它们分开),将候选类型T 扩大到U 中类型的某个子集。喜欢this。如果您有其他规则,那么它可能会被实施,但如果没有您所说的minimal reproducible example,我不知道如何提供帮助。此外,在 cmets 部分执行此操作可能也不是很好,因此如果这确实扩大到一个新问题的范围,您可能需要创建一个新帖子。祝你好运!
  • 您的游乐场演示正是我的意思。再次感谢!
【解决方案2】:

这是因为 stringnumber 本身具有重载的算术运算符,因为这是 JavaScript 中的一个特性。

在 JavaScript 中以下是有效的:

const x = "1";
const a = 1;

const s = x + a; // s = "11"

在 TypeScript 中,您可以执行以下操作:

const x: string = "1";
const a: number = 1;

const s = x + a; // s = "11", s is of type string

s 设置为输入number 不起作用:

const x: string = "1";
const a: number = 1;

const s: number = x + a; // TS compiler throws error

JavaScript 重载了+ 运算符,因此可以将字符串和数字相加(在这种重载情况下是串联的),结果是一个字符串。

【讨论】:

  • 这是一个很好的观点,但不幸的是没有回答主要问题。我会将return x + 1 替换为return 1return "1" 以避免离题的讨论。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-07-04
  • 2021-05-26
  • 2020-05-30
  • 1970-01-01
  • 1970-01-01
  • 2013-05-31
  • 2021-06-12
相关资源
最近更新 更多