TypeScript 中重载函数的类型约束

Iva*_*nin 4 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
  }
}
Run Code Online (Sandbox Code Playgroud)

它有效:

const x = myFunc(1)   // correctly inferred as number
const y = myFunc("1") // correctly inferred as string
Run Code Online (Sandbox Code Playgroud)

此语法但不保护重载实现中的混合类型:

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
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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
  }
}
Run Code Online (Sandbox Code Playgroud)

我老了:

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'.
Run Code Online (Sandbox Code Playgroud)

对于两个分支。基本上它和刚刚一样

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
  }
}
Run Code Online (Sandbox Code Playgroud)

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

jca*_*alz 6

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


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

粗略地说,允许实现的返回类型是所有调用签名的返回类型的联合,即使这忽略了任何特定调用签名的输入和输出之间的任何关系。只要 的实现myFunc()接受一个 type 参数number | string并返回一个 type 值number | string,编译器就会很高兴……即使number在相关调用签名声明它返回 a 的情况下,实现返回 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

在这里,我将预期的返回值保存到一个名为 的变量中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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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


至于函数通用版本的部分……首先,调用签名需要是这样的:

declare function myFunc<T extends number | string>(
  x: T): T extends number ? number : string;
Run Code Online (Sandbox Code Playgroud)

你不能返回T,因为文字类型的"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!
    }
}
Run Code Online (Sandbox Code Playgroud)

这是 TypeScript 的另一个缺失功能​​;编译器无法验证特定值(如x + "1")是否可分配给依赖于未指定泛型类型参数的条件类型。编译器只是在尚未解析时推迟评估此类类型,因此对于编译器来说太不透明,无法查看是否为该类型的值。TT extends number ? number : stringx + "1"

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

目前,除非您在每个函数中使用类型断言,否则此类函数将倾向于给出各种编译器警告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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

那好吧!


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

Playground 链接到代码