扩展联合类型

Ale*_*ler 5 typescript

考虑一下我有这些 TypeScript 类型定义:

export type Command = { AggregateId: string}

export type AddUser = (command: Command) => Promise<{streamId: string}>
export type RemoveUser = (command: Command) => Promise<{streamId: string}>

type CommandHandler = 
    | AddUser 
    | RemoveUser
Run Code Online (Sandbox Code Playgroud)

有没有一种方法可以CommandHandler在库中定义而不AddUser需要RemoveUser“附加”到CommandHandler引用包含的库的项目中CommandHandler

jca*_*alz 10

首先是一些术语……TypeScript 中的某些术语对之间存在二元性,并且经常会造成混淆。“扩展”一词通常指缩小类型以使其更具约束力。就您而言,当您说要“扩展”联盟时,您的意思与extends. 相反,您希望扩大类型以减少其限制。在 TypeScript 中没有一个很好的类似术语……你可以super用 Java 之类的东西来称呼它。无论如何,需要明确的是,您希望在库中有一个空的联合,并允许其他模块向其中添加成分,从而扩大(而不是扩展)它。

有一种称为声明合并的语言功能,它允许您通过“重新打开”模块/命名空间/接口并向其添加属性/方法来扩充现有类型。乍一看,这似乎没有帮助,因为您要更改的类型是类型别名(即type X = ...),并且您无法重新打开它们。而且您无论如何都不想向其中添加任何属性/方法。

这里的技巧是创建一个接口,其属性键是虚拟值,其属性值是您要查找的联合CommandHandlerMap的元素。CommandHandler然后你就可以定义type CommandHandler = CommandHandlerMap[keyof CommandHandlerMap]. 通过合并到CommandHandlerMap一个模块中,您将自动使CommandHandler联合获得一个组成部分。

代码可能如下所示:

库.ts

export type Command = { AggregateId: string };

export type CommandHandlerResult = { streamId: string };

export interface CommandHandlerMap {
  // will merge into this interface
}

export type CommandHandler = CommandHandlerMap[keyof CommandHandlerMap]
Run Code Online (Sandbox Code Playgroud)

添加用户.ts

import * as Library from './library';

export interface AddUserCommand extends Library.Command {
  username: string;
  somethingElse: number;
}

export type AddUser =
  (command: AddUserCommand) => Promise<Library.CommandHandlerResult>;

// reopen the CommandHandlerMap interface in the library module    
declare module './library' {
  export interface CommandHandlerMap {
    AddUser: AddUser // add this
  }
}
Run Code Online (Sandbox Code Playgroud)

删除用户.ts

import * as Library from './library';

export interface RemoveUserCommand extends Library.Command {
  username: string;
  withExtremePrejudice: boolean;
}

export type RemoveUser =
  (command: RemoveUserCommand) => Promise<Library.CommandHandlerResult>;

// reopen the CommandHandlerMap interface in the library module
declare module './library' {
  export interface CommandHandlerMap {
    RemoveUser: RemoveUser // add this
  }
}
Run Code Online (Sandbox Code Playgroud)

然后你可以验证它是否有效:

索引.ts

import * as Library from './library';
import { RemoveUser, RemoveUserCommand } from './removeUser';

const handler: Library.CommandHandler = async (c: RemoveUserCommand) => {
  return {
    streamId: c.username
  }
}; // okay
// const handler: AddUser | RemoveUser
Run Code Online (Sandbox Code Playgroud)

就目前而言,这是有效的。可以看到,Library.CommandHandler理解为AddUser | RemoveUser。请注意,CommandHandlerMap 恰好是"AddUser""RemoveUser",但这不是必需的。CommandHandlerMap我们可以用键"BlahBlah"或合并到"!!!@#$"。它们是虚拟密钥,只需唯一(因为您不想与现有密钥冲突)即可工作。

使用此方法的主要警告可能是,您在模块中引入的任何错误可能最终会在库代码而不是您的模块中被标记。例如,如果库中的代码假定这CommandHandler绝对是函数类型,并且模块中的某个人将非函数值合并到联合中,则可以预期错误会出现在库中。这有点难以调试。

无论如何,希望能给你一些想法。祝你好运!

链接到代码