扩展联合类型

时间:2019-06-09 10:01:22

标签: 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

是否有一种方法可以在没有CommandHandlerAddUser的库中定义RemoveUser,但可以在引用包含{的库的项目中将其“附加”到CommandHandler {1}}?

2 个答案:

答案 0 :(得分:2)

首先,一些术语……在TypeScript中,某些术语对之间存在双重性,而且常常会造成混淆。 “扩展”一词通常是指收窄一种使其受 more 约束的类型。就您而言,当您说要“扩展”联合时,您的意思与extends并不相同。相反,您想要扩大的类型以使其受 less 约束。在TypeScript中没有一个很好的类似术语……您可以在Java之类的语言中将其称为super。无论如何,要明确一点,您正在寻找一个在库中的空联合,并允许其他模块向其中添加成分,从而扩展(而不是扩展)。

有一个名为declaration merging的语言功能,通过“重新打开”模块/命名空间/接口并向其添加属性/方法,您可以扩展现有类型。乍看起来似乎无济于事,因为您要更改的类型是类型别名(即type X = ...),并且您无法重新打开它们。而且您也不想添加任何属性/方法。

这里的技巧是使一个类似于CommandHandlerMap的接口,其属性键是伪值,并且其属性值是您要寻找的CommandHandler并集的元素。然后,您可以定义type CommandHandler = CommandHandlerMap[keyof CommandHandlerMap]。通过合并到模块中的CommandHandlerMap中,您将自动使CommandHandler并集获得组成部分。

代码可能看起来像这样:

library.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]

addUser.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
  }
}

removeUser.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
  }
}

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

index.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

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

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

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

Link to code

答案 1 :(得分:0)

我敢肯定,在这样的事实之后,您将无法更改CommandHandler的含义。

您可以做的是将CommandHandler定义为:

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

然后在项目中,如果要使用AddUserRemoveUser类型,请为它们指定别名:

export type AddUser = CommandHandler;
export type RemoveUser = CommandHandler;