在不同模块中扩大 TypeScript 中的标记/歧视联合?

Ron*_*ton 7 strong-typing discriminated-union typescript

我有一个用于通过套接字连接来回传递 JSON 消息的系统。它使用标记联合来表示消息类型:

export type ErrorMessage = { kind: 'error', errorMessage: ErrorData };
export type UserJoined = { kind: 'user-joined', user: UserData };
// etc
export type Message = ErrorMessage | UserJoined | /*etc*/;
Run Code Online (Sandbox Code Playgroud)

它在基本代码中运行得相当好,但我在其之上构建了一个模块,我想扩展代码。我要添加一个新的消息类型:

export type UserAction = { kind: 'user-action', action: Action }
Run Code Online (Sandbox Code Playgroud)

这里的问题是我无法扩展“Message”以将我的新 UserAction 包含到联合中。我想我可以制作自己的扩展消息:

export type ExtendedMessage = Message | UserAction;
Run Code Online (Sandbox Code Playgroud)

但这里的问题是,第一,这看起来很笨拙。我无法将新的 UserAction 传递到任何需要消息的方法中,即使代码实际上应该完全正常工作。以后想要扩展我的模块和基本模块的任何人都需要创建第三种类型:export type ExtendedMessageAgain = ExtendedMessage | MyNewMessage

所以。我已经看到通过添加新的 .d.ts 文件来扩展附加属性的接口(例如 Passport 如何扩展 Express JS 的 Request 对象以添加身份验证属性),我认为标记联合也必须存在类似的东西,对吗?

但事实似乎并非如此。我用谷歌搜索了一下,没有看到这种模式在任何地方使用。这让我相信我的设计也许在某种程度上是错误的。但我没有看到解决办法。

我不想使用类,因为类型信息会通过网络被删除;该kind财产必须存在。我喜欢这个范例:

declare var sendMessage = (message: Message) => void;
sendMessage( { kind: 'error', errorMessage: { /* */ } }); // ok
sendMessage( { kind: 'random', parameter: { /* */ } }); // error, no kind 'random'
sendMessage( { kind: 'error', message: { /* */ } }); // error, no property 'message' on 'error'
Run Code Online (Sandbox Code Playgroud)

但我看到的唯一解决方案是制作Message一个界面基础,如下所示:

export interface Message { kind: string }
export interface ErrorMessage extends Message { errorMessage: ErrorData }

declare var sendMessage = (message: Message) => void;

sendMessage( { kind: 'error', errorMessage: { /* */ } }); // ok
sendMessage( { kind: 'random', parameter: { /* */ } }); // ok
sendMessage( { kind: 'error', message: { /* */ } }); // ok
Run Code Online (Sandbox Code Playgroud)

这种方法失去了上面所有好的类型保护。

那么...有没有一种方法可以在多个模块中扩展标记联合,从而影响类型的原始名称,并且无需定义新类型?或者这里有我没有看到的更好的设计吗?

以下是启发这篇文章的代码: https: //github.com/RonPenton/NotaMUD/blob/master/src/server/messages/index.ts

我希望对其进行大规模重构,以便我可以将所有消息移至单独的模块中,而不是随着时间的推移这个文件变得一团糟。

Mer*_*aya 6

您可以执行此操作来定义 的联合类型Message

export interface MessageTypes {}
export type Message = MessageTypes[keyof MessageTypes]
Run Code Online (Sandbox Code Playgroud)

然后,无论您在何处定义新消息类型,请执行以下操作:

export type UserAction = { kind: 'user-action', action: Action }

declare module '../message' { // Where you define MessageTypes
  interface MessageTypes {
    UserAction: UserAction
  }
}
Run Code Online (Sandbox Code Playgroud)

因此,接口的值MessageTypes成为联合类型,您可以使用声明合并向接口添加更多值,这将自动更新联合类型。

您可以查看 TS 文档以获取有关声明合并的更多信息: https: //www.typescriptlang.org/docs/handbook/declaration-merging.html