如何在TypeScript中正确重载函数?

huangapple go评论76阅读模式
英文:

How to correctly overload functions in TypeScript?

问题

能有人告诉我这里类型有什么问题吗?我试图在TS中通过重载清楚表达自己,但下面的问题让我感到很困惑...

type myNumbers = 1 | 2 | 3
type myStrings = 'a' | 'b' | 'c';

interface ReturnValue {
  (tag: 'a'): 1
  (tag: 'b'): 2
  (tag: 'c'): 3
  (tag: myStrings): myNumbers
}

class Log {
  public returnValue: ReturnValue = (tag: myStrings): myNumbers => {
    switch (tag) {
      case 'a':
        return 1;
      case 'b':
        return 2;
      case 'c':
        return 3;
    }
  }
}
类型 '(tag: myStrings) => myNumbers' 无法赋值给类型 'ReturnValue'。
  类型 'myNumbers' 无法赋值给类型 '1'。
    类型 '2' 无法赋值给类型 '1'。
英文:

Can anyone please tell me what's the problem with types here? I'm trying to make myself clear with overloads in TS but that problem below makes me really confused...

type myNumbers = 1 | 2 | 3
type myStrings = 'a' | 'b' | 'c';

interface ReturnValue {
  (tag: 'a'): 1
  (tag: 'b'): 2
  (tag: 'c'): 3
  (tag: myStrings): myNumbers
}

class Log {
  public returnValue: ReturnValue = (tag: myStrings): myNumbers => {
    switch (tag) {
      case 'a':
        return 1;
      case 'b':
        return 2;
      case 'c':
        return 3;
    }
  }
}
Type '(tag: myStrings) => myNumbers' is not assignable to type 'ReturnValue'.
  Type 'myNumbers' is not assignable to type '1'.
    Type '2' is not assignable to type '1'.

答案1

得分: 4

以下是您要翻译的内容:

Overloads in TypeScript consist of multiple call signatures and a single implementation, which has its own signature. TypeScript never really "correctly" type checks the implementation against the call signatures. It's either checked too loosely and allows incorrect implementations, or too strictly and prohibits correct implementations. So there is no way to "correctly" overload functions in TypeScript. Instead you have to pick a direction in which to err.

如果您编写一个重载的 function 声明 或一个 class 方法,编译器将松散地检查实现。这使得编写重载变得容易,没有编译器错误:

function returnValue(tag: "a"): 1;
function returnValue(tag: "b"): 2;
function returnValue(tag: "c"): 3;
function returnValue(tag: MyStrings): MyNumbers;
function returnValue(tag: MyStrings) {
  switch (tag) {
    case 'a': return 1;
    case 'b': return 2;
    case 'c': return 3;
  }
}

class Log {
  public returnValue: ReturnValue = returnValue;
}

或者

class Log {
  public returnValue(tag: "a"): 1;
  public returnValue(tag: "b"): 2;
  public returnValue(tag: "c"): 3;
  public returnValue(tag: MyStrings): MyNumbers;
  public returnValue(tag: MyStrings) {
    switch (tag) {
      case 'a': return 1;
      case 'b': return 2;
      case 'c': return 3;
    }
  }
}

因此,这是您可以采用的一种方法:将函数表达式更改为函数声明或类方法。

但是,这种宽松性仅捕获实现签名与调用签名明显不兼容的最严重错误的情况。它不会捕获您混淆哪个返回类型与哪个调用签名的情况:

function returnValue(tag: "a"): 1;
function returnValue(tag: "b"): 2;
function returnValue(tag: "c"): 3;
function returnValue(tag: MyStrings): MyNumbers;
function returnValue(tag: MyStrings) {
  switch (tag) {
    case 'a': return 3; // 错误,但没有错误
    case 'b': return 1; // 错误,但没有错误
    case 'c': return 2; // 错误,但没有错误
  }
}

在某种程度上,函数声明和类方法重载只关心实现返回调用签名返回类型的联合

因此,请谨慎检查您的实现,如果您使用此类重载,因为编译器不能

另一方面,如果您尝试编写重载的 function 表达式箭头函数,则编译器只会在实现的返回类型可分配给所有调用签名的返回类型时进行类型检查,即它们的交集

const ret: ReturnValue = (tag: MyStrings) => {
  throw new Error();
} // 可以

const slightlyLessContrived: {
  (x: string): { a: string };
  (x: number): { b: number };
} = (x: string | number) => ({ a: "", b: 1 }); // 可以

第一行编译成功,因为ret的实现返回了never类型,当您尝试同时说一个值是1&2&3时,这就是您得到的...也就是,这是不可能的。第二行编译成功,因为实现返回了{a: string, b: number},这等同于{a: string}&{b: number}

但是这样的实现相当无用;如果您愿意返回返回类型的交集,那么您就不需要重载了。您可以直接使用实现。

目前没有选项可以编写函数表达式并从函数声明获取宽松的类型检查。有一个开放的功能请求 microsoft/TypeScript#47669,但迄今为止它不是语言的一部分。

但是,如果您想放宽函数表达式的类型检查,可以像放宽任何表达式的类型检查一样使用 类型断言

class Log {
  public returnValue = ((tag: MyStrings): MyNumbers => {
    switch (tag) {
      case 'a': return 1;
      case 'b': return 2;
      case 'c': return 3;
    }
  }) as ReturnValue; // 可以
}

但这与之前允许不安全的实现的问题相同:

class Log {
  public returnValue = ((tag: MyStrings): MyNumbers => {
    switch (tag) {
      case 'a': return 3; // 错误
      case 'b': return 1; // 我
      case 'c': return 2; // 搞砸了
    }
  }) as ReturnValue; // 仍然可以
}

同样,您必须小心谨慎。

因此,如果您想编写重载,这些是您的选择:要么编写函数声明或类方法,要么编写带有类型断言的函数表达式。是否“正确”是主观的。

[Playground链接到代码](https://www.typescriptlang.org/play?#code/C4TwDgpgBAsiByBXAtgIwgJwM5QLxQEYoAfKAJhKgGYAo

英文:

Overloads in TypeScript consist of multiple call signatures and a single implementation, which has its own signature. TypeScript never really "correctly" type checks the implementation against the call signatures. It's either checked too loosely and allows incorrect implementations, or too strictly and prohibits correct implementations. So there is no way to "correctly" overload functions in TypeScript. Instead you have to pick a direction in which to err.


If you write an overloaded function statement or a class method, the compiler will check the implementation loosely. This makes it easy to write overloads without compiler errors:

function returnValue(tag: "a"): 1;
function returnValue(tag: "b"): 2;
function returnValue(tag: "c"): 3;
function returnValue(tag: MyStrings): MyNumbers;
function returnValue(tag: MyStrings) {
  switch (tag) {
    case 'a': return 1;
    case 'b': return 2;
    case 'c': return 3;
  }
}

class Log {
  public returnValue: ReturnValue = returnValue;
}

or

class Log {
  public returnValue(tag: "a"): 1;
  public returnValue(tag: "b"): 2;
  public returnValue(tag: "c"): 3;
  public returnValue(tag: MyStrings): MyNumbers;
  public returnValue(tag: MyStrings) {
    switch (tag) {
      case 'a': return 1;
      case 'b': return 2;
      case 'c': return 3;
    }
  }
}

So that's one approach you can take: change your function expression to a function statement or a class method.

But that looseness only catches the most egregious of errors in which the implementation signature is definitely incompatible with the call signatures. It won't catch situations where you mix up which return type goes with which call signature:

function returnValue(tag: "a"): 1;
function returnValue(tag: "b"): 2;
function returnValue(tag: "c"): 3;
function returnValue(tag: MyStrings): MyNumbers;
function returnValue(tag: MyStrings) {
  switch (tag) {
    case 'a': return 3; // oops, but no error
    case 'b': return 1; // oops, but no error
    case 'c': return 2; // oops, but no error
  }
} 

In some sense function statement and class method overloads only care that the implementation returns the union of the call signature return types.

So remember to check your implementations carefully if you use such overloads, because the compiler can't


On the other hand, if you try to write an overloaded function expression or arrow function, the compiler will only type check if the implementation's return type is assignable to all of the call signature's return types, that is, their intersection.

const ret: ReturnValue = (tag: MyStrings) => {
  throw new Error();
} // okay

const slightlyLessContrived: {
  (x: string): { a: string };
  (x: number): { b: number };
} = (x: string | number) => ({ a: "", b: 1 }); // okay

The first line compiles because the implementation of ret returns the never type which is what you get when you try to say a value is 1 & 2 & 3 all at once... that is, it's impossible. And the second line compiles because the implementation returns {a: string, b: number} which is equivalent to {a: string} & {b: number}.

But such implementations are fairly useless; if you were willing to return the intersection of the return types, you wouldn't need overloads in the first place. You'd just use the implementation directly.

There currently is no option to write a function expression and get the loose type checking from function statements. There's an open feature request at microsoft/TypeScript#47669 for that, but so far it's not part of the language.

But, if you want to loosen type checking of a function expression you can do so the same way you loosen type checking for any expression: use a type assertion:

class Log {
  public returnValue = ((tag: MyStrings): MyNumbers => {
    switch (tag) {
      case 'a': return 1;
      case 'b': return 2;
      case 'c': return 3;
    }
  }) as ReturnValue; // okay
}

But this has the same problem as before in terms of allowing unsafe implementations:

class Log {
  public returnValue = ((tag: MyStrings): MyNumbers => {
    switch (tag) {
      case 'a': return 3; // oops
      case 'b': return 1; // I
      case 'c': return 2; // messed up
    }
  }) as ReturnValue; // still okay
}

Again, you'll have to be careful.


So those are your choices if you want to write overloads: either write a function statement or class method, or write a function expression with a type assertion. Whether or not this is "correct" is subjective, though.

Playground link to code

huangapple
  • 本文由 发表于 2023年6月1日 02:51:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/76376494.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定