提问人:QuantumNoisemaker 提问时间:4/26/2023 最后编辑:QuantumNoisemaker 更新时间:4/26/2023 访问量:103
如何将变量从字符串缩小到具有可区分联合的大型对象中嵌套对象的键
How to narrow a variable from a string to a key of a nested object in a large object with Discriminated unions
问:
我正在打字稿中使用双嵌套的 JSON 对象,当我尝试将字符串缩小到第一个嵌套字符串的键时,我的头一直在撞墙。将字符串缩小到主对象的键是微不足道的,如果只有几个嵌套对象,则可以使用 if-else 或 switch 可靠地处理。但是,大约有 40 个第 1 层嵌套对象,每个对象都有区分联合。我可以为每个特定对象设计一个很长的开关测试用例,但这会很乏味,并且可能最终会重复大量代码,所以我想找到一种更优雅的方法。
以下是该对象的示例:
const jsonObject = {
cow: {
milk: {
chemicalState: "liquid",
value: 10,
description: "for putting on cereal"
},
cheese: {
chemicalState: "solid",
value: 25,
description: "for putting on sandwiches"
},
},
pig: {
bacon: {
chemicalState: "solid",
value: 100,
description: "for putting on everything"
},
pork: {
chemicalState: "solid",
value: 50,
description: "for dinner"
},
},
//... and thirty more double nested objects from here
}
在这个例子中,每只动物将有一组完全不同的产品,彼此之间没有重叠。目标是编写一个函数,该函数可以在给定动物和产品的情况下从 jsonObject 中提取描述条目:
const getDescription = (animal: string, product: string):string => {
return jsonObject[animal][product].description
}
使用用户定义的类型保护来缩小第一个键(动物)的范围是微不足道的。但是,jsonObject[animal] 不会缩小到一个特定的产品对象,而是所有可能的产品对象的联合(前面提到的可区分联合)。在计算 jsonObject[animal] 之后,只使用了一种 animalProduct 对象类型,但 typescript 编译器无法知道是哪种类型,因此它不是返回单个 animalProduct 对象,而是重新合并了 32 个可能的 animalProducts,这在开关中需要处理很多。我现在也试图避免使用“as”中的保释金。
理想情况下,有一种方法可以编写一个通用/泛型用户定义的类型保护,以缩小第二个键的范围,就像第一个键一样;一个只允许所选动物的特定键通过(因此对于 jsonObject[cow],键类型应为 milk|cheese)。然而,这可能是不可能的或不切实际的,所以欢迎任何和所有的建议和技巧(对 Typescript 和学习来龙去脉仍然很陌生)!
我做了很多尝试来缩小两个键的范围,然后是先是可区分的并集,然后是键。我最接近的尝试可能是使用这个用户定义的泛型(受到这篇文章的启发,不幸的是它没有解决我的问题,因为它并没有消除对大量 switch 语句的需求):
const isAnimalProduct = <T extends string>(s:string, o: Record<T,any>): s is T => {
return s in o;
}
这很接近,但会给你输入 Record<“milk” |“奶酪” |“培根” |“猪肉”,任何>而不是 Record<“牛奶” |“奶酪”,任何> |唱片<“培根” |“猪肉”,任何>,似乎并没有在必要时缩小范围,但我仍然承认我的头在缠绕它。
下面是 Playground 链接,其中包含代码的完整模型和更多尝试。
我还在相关线程上查看了此响应和此响应,但我无法获得类似的结果,因为两者都使用硬编码值,并且在我的情况下,这两个值都是变量,并且您最终仍然会得到一个可区分的并集(但如果我弄错了,请告诉我)。
另外,据我所知,这个问题与 JSON 无关,因为我设置了“resolveJsonModule”: true,并且打字稿似乎在导入它时没有问题。从游乐场链接中可以看出,即使使用纯对象,问题仍然存在。
编辑:输入和 getDescription 来自用户输入(特别是两个组合框)。我已将产品的第二个组合框配置为仅接受基于组合框状态的正确输入(如果用户在第一个组合框中选择奶牛,则第二个组合框将仅显示牛奶和奶酪)。但是,我试图防止的行为是当用户返回并更改第一个组合框(即更改为 pig)时,导致键不匹配(例如 pig、milk)。animal
product
animal
animal
JCalz 建议我
将 jsonObject 扩大为双重嵌套的索引签名,并测试是否为 undefined。
我的理解是,这应该防止这种情况,因为如果将 Pig 和 Milk 提供给索引签名,这将被正确地检测为未定义。我现在将完全实现它以确认!
具有更新上下文的新 Playground 链接
答:
编译器在跟踪相关联合类型方面不是很好(请参阅 microsoft/TypeScript#30581),因此您的原始方法只会给您带来麻烦。通常,最好用泛型而不是联合来写东西,但在你的例子中,结构可以用索引签名更简单地表示:jsonObject
const j: Dictionary<Dictionary<Product>> = jsonObject;
interface Dictionary<T> {
[k: string]: T | undefined
};
interface Product {
chemicalState: string;
value: number;
description: string;
}
我已将(我假设它来自外部导入的文件,因此您无法更改其类型)重新分配给 ,其类型为 .该赋值成功,因此编译器认为它们是兼容的。A 只是一个具有未知键的对象类型,其值为 either 或 undefined。因此,类型是具有未知键的对象,其值要么是未定义的,要么是另一个具有未知键的对象,其值要么是未定义的,要么是上面定义的。您现在所要做的就是检查何时索引到 ,如下所示:jsonObject
j
Dictionary<Dictionary<Product>>
Dictionary<T>
T
j
Product
undefined
j
const getDescription = (animal: string, product: string): string => {
return j[animal]?.[product]?.description ?? "not valid";
}
这是使用可选的 chaining (?.)
运算符和 nullish 合并 (??
) 运算符来处理 s。如果存在,则返回。否则,将返回 (而不是索引错误),并且 ,又名。undefined
j[animal][product].description
getDescription
j[animal]?.[product]?.description
undefined
undefined ?? "not valid"
"not valid"
评论
jsonObject (which I assume came from an external imported file so you can't change its type)
import lvls from './json/CharLvl.json'
"resolveJsonModule": true,
import _lvls from './json/CharLvl1.json'
const lvls: Dictionary<Dictionary<Product>> = _lvls
lvls
上一个:通用 Typeguard
评论
item
chemicalState
jsonObject
undefined