我有联合类型ItemItemSlot,以及将Item映射到Slot的映射类型.

type WeaponSlot = "melee" | "ranged";

type ArmorSlot = "helmet" | "gloves" | "boots";

type Weapon = {
  type: "weapon";
  slot: WeaponSlot;
  damage: number;
};

type Armor = {
  type: "armor";
  slot: ArmorSlot;
  armorValue: number;
};

type Item = Weapon | Armor;

type ItemSlot = WeaponSlot | ArmorSlot;

// type ItemSlots = {
//     melee: Weapon;
//     ranged: Weapon;
//     helmet: Armor;
//     gloves: Armor;
//     boots: Armor;
// }
type ItemSlots = {
  [O in Item as O["slot"]]: O;
};

项目和项目槽之间有明确的关系,这在类型映射中得到了反映.但是我想不出如何利用这种关系,所以我可以创建一个可以得到Item的泛型函数,并以一种类型安全的方式将它映射到正确的ItemSlot.

这些都是有效的

// manual way
function putItem(item: Item, slots: ItemSlots) {
  if (item.slot === "boots") {
    slots["boots"] = item;
  } else if (item.slot === "melee") {
    slots["melee"] = item;
  }
}


// can do generic when slot is passed from outside
function putItem2<S extends ItemSlot>(
  item: ItemSlots[S],
  slots: ItemSlots,
  slot: S,
) {
  slots[slot] = item;
}

但我不想要手动选项.并且插槽可能不会从外部传递,如下例所示

// a function that takes an item and puts it in its corresponding slot
function putItem3(item: Item, slots: ItemSlots) {
  slots[item.slot] = item;
}

// or a function that takes an item list and slot as an input and
// filters items to get an item suitable for the slot
function putItem4(items: Item[], slot: ItemSlot, slots: ItemSlots) {
  // just return the first item that suits to slot
  const item = items.filter((item): item is Something => item.slot === slot)[0];

  slots[item.slot] = item;
}

不知何故,我需要提示,该项目有一个插槽,将指向正确的插槽之一.我需要在Slot上设置Item通用吗?这将如何与unions 类型一起工作?或者,有没有更好的解决方案,也许不需要使用更多的泛型?

这似乎不是一个独一无二的问题.如果您能链接一些以泛型方式处理联合类型的自然示例,我也将不胜感激.

推荐答案

你遇到了一个我称之为"相关联合类型"的问题,如microsoft/TypeScript#30581中所讨论的. 有时候,人们编写的代码中,一个不变的值union type被多次使用,如果你记住这个值的类型在所有情况下都必须是相同的联合成员,那么这显然是安全的. 例如,如果x的类型是number[] | string[],那么x.push(x[0])是安全的(假设x非空),因为x要么两次都是number[],要么两次都是string[]. 但是编译器不会以这种方式分析代码,因为它不跟踪值的identity,只是跟踪类型. 它就像你写x1.push(x2[0])一样,其中x1x2都是number[] | string[]类型. 显然是unsafe.

这就是你的putItem()代码的问题所在.slots[item.slot]item必须是同一类型的,但只有当你追踪itemidentity而不是它的类型时,才能发现这一点.


对关联联合的推荐方法是在microsoft/TypeScript#47109中描述的重构,其中您从联合转移到generics,从constrained转移到联合,具体涉及"基本"接口和接口的泛型indexes into,并通过该接口转移到mapped types.这样做的目的是让这两个相关的表达式被视为可视等价的generic类型.

在您的示例中,它可能如下所示.首先是基本接口:

type ItemMap = { [I in Item as I["type"]]: I }
/* type ItemMap = {
    weapon: Weapon;
    armor: Armor;
} */

然后,ItemSlots类型成为以下形式的泛型类型:

type ItemSlots<K extends keyof ItemMap> = Record<ItemMap[K]["slot"], ItemMap[K]>;

如果你想要的话,你可以保持原来的ItemSlots,但重点是ItemSlots<"weapon">ItemSlotsWeapon合适的部分,ItemSlotes<"armor">Armor合适的部分.

然后,可以通过以下方式使putItem()成为通用的:

function putItem3<K extends keyof ItemMap>(item: ItemMap[K], slots: ItemSlots<K>) {
  const slot: ItemMap[K]['slot'] = item.slot
  slots[slot] = item;
}

请注意,对于microsoft/TypeScript#33181有一点变通办法,因为当您使用特定键(如"slot")索引泛型(如ItemMap[K])时,编译器会将其扩展到非泛型. 所以我需要annotateslot是类型ItemMap[K]['slot'],因为这是ItemSlots<K>的关键.

您的filter()版本也是类似的通用版本:

function putItem4<K extends keyof ItemMap>(
  items: Item[], slot: ItemMap[K]["slot"], slots: ItemSlots<K>
) {
  const item = items.filter((item): item is ItemMap[K] => item.slot === slot)[0];
  slots[slot] = item;
}

所以这是可行的,至少是模糊的类型安全,特别是与只使用type assertions相比.如果编译器可以通过将其分析分发到联合或跟踪身份来始终"看到"安全性,那将是很好的,但就目前而言,微软/TypeScrip#47109是我们解决此类问题的最佳方法.

Playground link to code

Typescript相关问答推荐

如何从具有给定键列表的对象类型的联合中构造类型

如何正确修复TypScript 4.7到4.8升级中的TS 2345编译错误

如何使用axios在前端显示错误体

用泛型类型覆盖父类函数导致类型y中的Property x不能赋给基类型Parent中的相同属性."'''''' "

类型脚本强制泛型类型安全

在Angular中注入多个BrowserModules,但在我的代码中只有一个

在配置对象上映射时,类型脚本不正确地推断函数参数

如何通过TypeScript中的工厂函数将区分的联合映射到具体的类型(如类)?

具有继承的基于类的react 组件:呈现不能分配给基类型中的相同属性

在映射类型中用作索引时,TypeScrip泛型类型参数无法正常工作

如何在LucideAngular 添加自定义图标以及如何使用它们

使用数组作为类型中允许的键的列表

如何合并两个泛型对象并获得完整的类型安全结果?

有没有一种方法可以确保类型的成员实际存在于TypeScript中的对象中?

对象类型的TypeScrip隐式索引签名

为什么&Quot;元素隐式具有';Any';类型,因为...即使我已经打字了,也不能作为索引显示错误吗?

完全在类型系统中构建的东西意味着什么?

无法使用prisma中的事务更新记录

将对象数组传递给 Typescript 中的导入函数

如何编写一个接受 (string|number|null|undefined) 但只返回 string 且可能返回 null|undefined 的函数?