从打字稿中的部分受歧视的联合中检测类型

Detecting a type from a partial of a discriminated unions in typescript

我在制作类型检测函数时遇到问题,该函数从已区分的联合中获取部分字段,并从联合中获取匹配类型 return。我有一个 create() 函数,它接受不包括时间戳和 ID 的字段,我 return 那些带有 ID 和时间戳的字段。我们在参数和 return 对象中有 type 字段,但它没有用于确定 return 类型。

Playground Link

interface Node {
  type: string
  id: string
  createdAt: number
  updatedAt: number
}

interface Pokemon extends Node {
  type: 'Pokemon'
  name: string;
}

interface Trainer extends Node {
  type: 'Trainer'
  name: string;
  pokemon: string[]
}


type CreateNode<T extends Node> = T extends unknown
    ? Omit<T, 'id' | 'createdAt' | 'updatedAt'>
    : never

function create<R extends Node>(obj: CreateNode<R>): R {
  return {
    ...obj,
    id: 'ds',
    createdAt: 4,
    updatedAt: 5,
  } // give as error "... as is assignable to the constraint of type 'R', but 'R' could be instantiated with a different subtype of constraint 'Node'."
}

const pokemon = create<Pokemon|Trainer>({
  type: 'Pokemon',
  name: 'Garalaxapon'
})

pokemon //should be Pokemon but is Pokemon | Trainer

我不明白 return 中的错误,但我确定这就是问题的症结所在。谢谢!

pokemon //should be Pokemon but is Pokemon | Trainer

这是正确的。原因:

您有 const pokemon = create<Pokemon|Trainer>,其中 create<R extends Node>(obj: CreateNode<R>): R。因为createreturns传入的是R,pokemon就是你传入的R。你传入的是Pokemon|Trainer所以pokemon: Pokemon|Trainer

解决方案

如果这是您想要的,请使用 Pokemon,即 create<Pokemon>

我将忽略实施中的问题,这是与您询问的问题不同的问题。现在我将假设实现有效,并使用 declare statement 来关注调用签名问题。


您面临的问题是您对单个 R 通用类型的期望过高。您手动指定它是为了让编译器知道您想要区分哪个特定的 Node 子类型联合。但是您还希望编译器以某种方式将 R 缩小到该联合的成员之一。这是行不通的。一旦您手动将 R 指定为一个联合体,它就是永远的联合体。

您可以使用两个通用参数来解决这个问题;一个 (R) 您为可区分的联合指定,一个 (T) 编译器从 passed-in 参数推断。不幸的是,您不能在单个函数签名中执行此操作;您需要手动指定 RT,否则编译器将尝试推断 RT。 TypeScript 中没有 partial type parameter inference (microsoft/TypeScript#26242)

有时在这种情况下,我会使用类型参数 currying 将两个类型参数的单个函数拆分为多个 one-type-parameter 函数。在您的情况下,它看起来像这样:

declare function create<R extends Node>(): <T extends CreateNode<R>>(obj: T) => Extract<R, T>;

注意 R 如何对应于完整的可区分联合,而 T 指的是 passed-in obj 参数的类型。您通过使用 Extract 实用程序类型将 RT 区分开来:return 只有 R 中可分配给 T 的元素:

const createPokemonOrTrainer = create<Pokemon | Trainer>();

const pokemon = createPokemonOrTrainer({
  type: 'Pokemon',
  name: 'Garalaxapon'
}); // Pokemon

在这里,create() return 是另一个函数,create<Pokemon | Trainer>() return 是您之前尝试创建的函数:接受 partial-ish Pokemon | Trainer 并将其区分为 PokemonTrainer.


但也许您实际上根本不需要 R;你打算每次都用不同的受歧视工会打电话给 create() 吗?如果你只打算使用像 Pokemon | Trainer 这样的单一联合类型,那么你可以只对它进行硬编码。本质上,不要为 over-generic create() 而烦恼,只需手动编写 createPokemonOrTrainer():

type DiscrimUnion = Pokemon | Trainer;
declare function createDiscrimUnion<T extends CreateNode<DiscrimUnion>>(obj: T): Extract<DiscrimUnion, T>;

const pokemonAlso = createDiscrimUnion({
  type: 'Pokemon',
  name: 'Garalaxapon'
}); // Pokemon

好的,希望对您有所帮助;祝你好运!

Playground link to code