【问题标题】:How to ensure a TypeScript argument is a union with a specific type如何确保 TypeScript 参数是具有特定类型的联合
【发布时间】:2019-11-21 02:10:25
【问题描述】:

我如何编写一个 TypeScript 函数来确保一个参数是一个恰好包含另一个(更具体的)联合的联合?例如:

type Command = { 
  name: string 
  [key: string]: any
}

type Insert = { name: 'insert'; text: string }
type Delete = { name: 'delete'; position: number }
type CoreCommand = Insert | Delete

type Replace = { name: 'replace'; position: number; text: string }
type CustomCommand = CoreCommand | Replace

const exec = <T ???>(command: T) => {
  switch (command.name) {
    case 'insert':
      // ...
      break
    case 'delete':
      // ...
      break
    // ...
  }
}

在上面的代码中,如何确保T 泛型类型保证包含CoreCommand 联合类型,同时仍然满足其他“自定义”命令?

能否确保自定义命令与核心命令不冲突?

【问题讨论】:

  • TypeScript 没有下界泛型,所以你不能这样做,为什么不用像 &lt;T&gt;(command: T | CoreCommand) =&gt; ... 这样的东西?
  • 嘿@jcalz,它看起来确实与那个相似。我认为T | CoreCommand 的问题在于,由于我不能保证TCoreCommand 不会发生冲突,那么如果我在执行程序中使用switch,我不能保证command.name === 'insert_text' 意味着我正在使用核心 Insert 命令了。
  • 我想真正的问题是如何扩展 discriminated 联合并保持歧视有效?同时允许“核心”功能接受任何潜在的扩展,而不关心扩展位?
  • Exclude&lt;T, { name: CoreCommand['name'] }&gt; | CoreCommand 那么呢?

标签: typescript generics union


【解决方案1】:

问题是您的CustomCommand 已经包含CoreCommand 作为表达式CoreCommand | Replace 等于Insert | Delete | Replace。在您的情况下,我真的看不到任何多态需求,只需使用CustomCommand

const exec = (command: CustomCommand) => {
  switch (command.name) { // command.name is properly insert | delete | replace
    case 'insert':
      // ...
      break
    case 'delete':
      // ...
      break
    // ...
  }

另外,我们如何确保您创建与Command 接口匹配的新命令。我们可以通过条件类型来检查名称是否不在CoreCommands中:

type CreateCommand<T extends Command> = T['name'] extends CoreCommand['name'] ? never : T; 
type Insert2 = CreateCommand<{ name: 'insert'; position: number; text: string }>

type CustomCommand = CoreCommand | Insert2 // it will evaluate to Insert | Delete only

CreateCommand 将评估为 never 如果我们将传递带有 name 属性的类型,该属性已存在于核心命令中。因此,在联合过程中将跳过使用相同名称字段的类型。

您还可以创建额外的实用程序类型来创建始终包含 CoreCommands 的联合:

type Replace = CreateCommand<{ name: 'replace'; position: number; text: string }>
type Change = CreateCommand<{ name: 'change'; position: number; text: string }>

type MakeCommands<NewCommand extends Command> = CoreCommand | NewCommand
type CustomCommand = MakeCommands<Replace | Change>; // hre we always add CoreCommand to the list of commands

最后但并非最不重要的一点是如何使 exec 函数具有多态性:

const exec = <C extends Command>(command: C, f: (x: C) => void) => f(command);

const comm = { name: 'replace', position: 1, text: 'text' };
// below `as` is only used to block direct inference by TS from the const
// it is not needed for real code
const result = exec(comm as CustomCommand, (incommand) => {
  switch (incommand.name) { // icommand is here CustomCommand
    case 'insert':
      // ...
      break
    case 'delete':
      // ...
      break
    // ...
  }
});

exec 以上将从第一个参数推断类型,并且已经推断的类型将传递给作为第二个参数的函数。多亏了这一点,我们将指定具有 CoreCommands 的类型,以及作为 command 参数传递的类型中包含的任何命令。

【讨论】:

  • 谢谢!问题是exec 是在“核心”中定义的,而自定义命令是在其他地方定义的。因此,当定义 exec 时,还没有自定义命令。但我希望它能够接受自定义命令以及核心命令,用于其他开发人员想要进行的任何扩展。
  • 正是最后一个 sn-p 允许的。它由第一个参数决定,唯一的要求是包含核心命令。传递的函数可以在不同的范围内传递。
猜你喜欢
  • 2020-07-31
  • 2019-03-26
  • 1970-01-01
  • 1970-01-01
  • 2019-10-27
  • 1970-01-01
  • 1970-01-01
  • 2022-11-18
  • 2020-04-28
相关资源
最近更新 更多