TypeScript class 方法重载的行为与函数重载不同

TypeScript class method overloads not behaving the same as function overloads

如果这是重复的,请提前致歉,但我的搜索没有找到任何与我遇到的问题完全吻合的内容。

首先,期望的行为是有一个带有两个参数的 class 方法,第二个参数是可选的,其中第二个参数的类型取决于第一个参数的类型。如果第一个参数是 A 类型,则第二个参数应始终是必需的且应为 X 类型,如果第一个参数是 B 类型,则应省略第二个参数。

我已经通过函数重载实现了类似的东西:

// types
enum MessageType { FOO, BAR, BAZ }

type MessagePayload<T extends MessageType> = T extends MessageType.FOO 
    ? string
    : T extends MessageType.BAR
    ? number
    : never;

// overloads
function sendMessage<T extends MessageType.BAZ>(action: T): void

function sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void

// implementation
function sendMessage<T extends MessageType>(action: T, payload?: MessagePayload<T>) {
  // do something
}

// tests
sendMessage(MessageType.FOO, "10") // no error - as expected
sendMessage(MessageType.FOO, 10)   // error - as expected, payload is not string
sendMessage(MessageType.FOO)       // error - as expected, payload must be string
sendMessage(MessageType.BAZ);      // no error - as expected - since MessageType is BAZ

但是,将完全相同的构造应用于 class 方法时,不会产生相同的结果。此片段是第一个片段的延续,并使用相同的类型:

// interface
interface ISomeClient {
  sendMessage<T extends MessageType.BAZ>(action: T): void

  sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void
}

// implementation
class SomeClient implements ISomeClient {
  sendMessage<T extends MessageType>(action: T, payload?: MessagePayload<T>) {
    // do something
  }
}

// tests
const client = new SomeClient();

client.sendMessage(MessageType.FOO, "10"); // no error - as expected
client.sendMessage(MessageType.FOO, 10);   // error, payload is not string
client.sendMessage(MessageType.FOO)        // no error??? different behavior than function example
client.sendMessage(MessageType.BAZ);       // this part works fine

TS Playgound 上有一个更完整的示例。

所以,我猜这是一个二人组:

  1. 为什么这不适用于 class 示例?
  2. 是否有更好的方法来实现此目的,既适用于 classes 又适用于函数,并且不需要维护重载来捕获不需要负载的类型?我在这里使用了一个枚举和一个条件类型来约束第二个参数以匹配给定第一个参数的预期。我玩过 another way 涉及键入映射的键,但它看起来很老套,仍然需要重载,并且 classes 和函数也遇到同样的问题。

谢谢。

1. why is this not working for the class example?

我认为问题在于 class 中的签名不像第三个独立函数签名那样仅被视为实现签名,因为重载是单独声明的。所以 classaugmenting 那些,添加第三个 public 签名,与第三个签名不是 public 的函数重载相反,它是只是 implementation signature.

您可以通过不(仅)在接口声明中放置重载来修复它。要么不使用接口:

class SomeClient {
  sendMessage<T extends MessageType.QAT | MessageType.QAZ>(action: T): void;
  sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void;
  sendMessage<T extends MessageType>(action: T, payload?: MessagePayload<T>) {
    // do something
  }
}

Playground example

...或者确实使用接口,但也在 class 构造中重复重载,以便 TypeScript 知道第三个是实现签名:

interface ISomeClient {
  sendMessage<T extends MessageType.QAT | MessageType.QAZ>(action: T): void
  sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void
}

class SomeClient implements ISomeClient {
  sendMessage<T extends MessageType.QAT | MessageType.QAZ>(action: T): void
  sendMessage<T extends MessageType>(action: T, payload: MessagePayload<T>): void
  sendMessage<T extends MessageType>(action: T, payload?: MessagePayload<T>) {
    // do something
  }
}

Playground example

这是重复的,但我不确定除了分配给 SomeClient.prototype after-the-fact.

之外还有其他解决方法

2. is there some better way to achieve this...

我倾向于为此使用函数重载,但确实它们并非适用于所有情况,如果你有很多这样的函数,它很快就会变得笨拙。

我应该指出,我仍然只是 TypeScript 的初学者,所以可能还有其他选择,但我可以想到两个选择:

  1. 使用具有不同元组类型的剩余参数

  2. 使用有区别的联合,所以总是只有一个参数

在我发现函数重载太麻烦的地方,我倾向于区分联合,但元组的想法有点可爱,所以我想我会包括它。

休息 + 元组

而不是 MessagePayload<T>,您 MessageParams<T> 定义了一个基于 T 的元组:

type MessageParams<T extends MessageType> = T extends MessageType.FOO 
    ? [T, string]
    : T extends MessageType.BAR
    ? [T, number]
    : T extends MessageType.BAZ
    ? [T, User]
    : [T];

(如果其他原因需要MessagePayload<T>,可以从上面推导:type MessagePayload2<T extends MessageType> = MessageParams<T>[1];。)

然后该方法将其用作剩余参数的类型:

class SomeClient {
  sendMessage<T extends MessageType>(...args: MessageParams<T>) {
    const action = args[0];
    // do something
  }
}

Playground example

不过,开发人员体验非常像重载。

歧视联盟

最后一个选项是一个更大的变化:您根本没有单独的参数,只有一个对象类型,它是一个可区分的联合:

type FOOMessage = {action: MessageType.FOO; payload: string;};
type BARMessage = {action: MessageType.BAR; payload: number;};
type BAZMessage = {action: MessageType.BAZ; payload: User;};
type OtherMessage = {action: Exclude<MessageType, MessageType.FOO | MessageType.BAR | MessageType.BAZ>;};
// `OtherMessage` is the catch-all for all message types other than the
// ones with their own interface, note the use of `Exclude`

type Message = FOOMessage | BARMessage | BAZMessage | OtherMessage;

// ...

class SomeClient {
  sendMessage(message: Message) {
    const action = message.action;
    // do something
  }
}

对它的调用更改为传递一个对象:

// tests
client.sendMessage({action: MessageType.FOO, payload: "string"});
client.sendMessage({action: MessageType.FOO}); // Error as desired
client.sendMessage({action: MessageType.QAT});

Playground example