typescript - 我如何让我的函数专门针对联合类型标签?
我有联合类型Item和ItemSlot,以及一个映射类型,它将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;
}
我需要以某种方式提示该物品有一个插槽,可以指向其中一个正确的插槽。我需要让Item对Slot使用泛型吗?这与联合类型如何配合使用?或者是否有更好的解决方案可能不需要使用更多的泛型?
这似乎不是一个独特的问题。如果你能链接一些在实际中以通用方式处理联合类型的例子,我也将不胜感激。
答案#1
你遇到的问题我称之为“相关联合类型”,如 microsoft/TypeScript#30581 所讨论的。有时候人们会写出这样的代码:一个不变的联合类型值被多次使用,如果你记住该值的类型在所有出现中必须是相同的联合成员,那么这是显然安全的。例如,如果 x 的类型是 number[] | string[],那么 x.push(x[0]) 是安全的(假设 x 非空),因为 x 两次都是 number[] 或者两次都是 string[]。但是编译器不会这样分析代码,因为它并不追踪值的身份,只关注类型。它会把它当作你写了 x1.push(x2[0]) 来处理,其中 x1 和 x2 的类型都是 number[] | string[]。而这显然是不安全的。
这就是你在 putItem() 代码中遇到的问题。slots[item.slot] 和 item 必须是相同类型的,但这只有在你跟踪 item 的 身份 而不是它的类型时才能发现。
处理相关联合的推荐方法是在 microsoft/TypeScript#47109 中描述的重构,其中你从联合类型转向受联合约束的泛型,特别是涉及一个“基础”接口以及对该接口和该接口上的映射类型的泛型索引。目标是使两个相关表达式被视为具有可明显等价的泛型类型。
如你的示例所示,它可能看起来像这样。首先是基础接口:
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"> 被视为 Weapon 对应的 ItemSlots 部分,而 ItemSlots<"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])时,编译器会将其扩展为非泛型。因此我需要注释 slot 的类型为 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;
}
这样做可行,并且至少在一定程度上是类型安全的,尤其是相比仅使用类型断言来说。如果编译器能够通过在联合类型上分配其分析或跟踪身份来始终“看到”安全性,那就好了,但目前 microsoft/TypeScript#47109 是我们对于这类问题所拥有的最佳方法。
代码的 Playground 链接
