【问题标题】:Extend Union Type扩展联合类型
【发布时间】:2019-06-10 01:46:07
【问题描述】:

考虑一下我有这些 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

有没有办法在没有AddUserRemoveUser 的库中定义CommandHandler,但在引用包含CommandHandler 的库的项目中“附加”到CommandHandler

【问题讨论】:

  • 你这样做的用例是什么?由于AddUserRemoveUser 实际上是同一类型,你想用那个联合类型做什么?
  • 请参阅我对您的回答的评论。我会相应地更新我原来的问题。
  • 你可以使用声明合并来做到这一点......如果在我到达真机时没有其他人发布解决方案,那么我会这样做

标签: typescript


【解决方案1】:

首先是一些术语...... TypeScript 中的某些术语对之间存在双重性,并且经常混淆。 “扩展”一词通常是指缩小一个类型以使其更多受到约束。就您而言,当您说要“扩展”联合时,您的意思与extends 的含义不同。相反,您希望加宽类型以使其更少受到约束。在 TypeScript 中没有一个很好的类似术语......你可以在 Java 之类的东西中称它为super。无论如何,要明确的是,您希望在您的库中有一个空的联合,并允许其他模块向其中添加成分,从而扩大(而不是扩展)它。

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

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

代码可能如下所示:

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 keys 恰好是 "AddUser""RemoveUser",但这不是必需的。我们本可以使用键 "BlahBlah""!!!@#$" 合并到 CommandHandlerMap。它们是虚拟键,只需要唯一(因为您不想与现有键发生冲突)即可工作。

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

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

Link to code

【讨论】:

  • 感谢您的详细回答,这似乎可行,我会尝试一下。特别感谢关于扩大与延伸的解释。我也尝试了另一种方法,稍后将在一个新问题中发布一些关于它的问题。
【解决方案2】:

我相当肯定你不能改变 CommandHandler 的含义,因为事实如此。

您可以将CommandHandler 定义为:

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

然后在你的项目中,如果你想要AddUserRemoveUser 类型,给它们设置别名:

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

【讨论】:

  • 是否可以通过例如AddUserCommand(派生自Command)到AddUser,而RemoveUser不会接受?
  • @AlexanderZeitler - 恐怕我不知道。如果AddUserCommandRemoveUserCommand 不添加任何功能,它们最终可以相互分配。也就是说,我的第一个想法(下一条评论)不起作用,因为它们是赋值兼容的。当然,如果您将属性添加到 AddUserCommand 并将不同的属性添加到 RemoveUserCommand,那将起作用。
  • 但是如果你有 interface AddUserCommand extends Command { type: "add" }interface RemoveUserCommand extends Command { type: "remove" } 它们就不再兼容分配了。
猜你喜欢
  • 2019-11-02
  • 1970-01-01
  • 1970-01-01
  • 2013-08-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-25
相关资源
最近更新 更多