首页 > 解决方案 > TypeScript 中重载函数的类型约束

问题描述

所以我可以重载函数:

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
  }
}

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

标签: typescript

解决方案


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


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

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

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

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


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

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) && ...&&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;

您不能 return T,因为文字类型喜欢"hello"并且123存在,并且您不想声称myFunc(123)return 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")是否可分配给依赖于未指定泛型类型参数的条件类型。编译器只是在尚未解决时推迟评估此类类型,因此对于编译器来说太不透明,无法查看是否是该类型的值。TT extends number ? number : stringx + "1"

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

目前,除非您在 each 处使用类型断言,否则此类函数往往会给出各种编译器警告return

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 代码链接


推荐阅读