有没有办法在 Typescript 中为嵌套键访问创建类型保护?



我正在重新设计以前使用vanilla Javascript制作的实现。我遇到了一个有趣的情况,我确实有一个解决方案,但我认为我有一个更好的解决方案,但似乎不起作用。

基本上,我有一个包含两个嵌套对象的对象,它们的索引类型化为键的字符串文字,其中两个对象都有其他一些对象键,但没有完全重叠。然后,我有一个可以接收任一键的函数,以及用于访问该对象中的一个值的第二个键。我为密钥创建自定义类型保护,并创建了第二组类型保护来确认传递的第二个密钥是其中一个对象的密钥。然后,我创建了一个函数,该函数接受两个键,并且应该返回一个具有正确匹配键的对象。但是,打字稿在函数中似乎并不相信我正在使用我的验证器函数,从我的函数返回的密钥只有一个绝对可以访问上层对象的密钥。

这是一个非常不清楚的解释,我认为一些示例代码会让它变得更好,所以这里是:

const decoder = {
foo: {
foo: "bar",
bar: "foo"
},
bar: {
foo: "bar",
},
};
type FooKeys = keyof typeof decoder["foo"];
type BarKeys = keyof typeof decoder["bar"];
const isFooKey = (key: string): key is FooKeys => Object.prototype.hasOwnProperty.call(decoder["foo"], key);
const isBarKey = (key: string): key is BarKeys => Object.prototype.hasOwnProperty.call(decoder["bar"], key);
const isFoo = (key: string): key is "foo" => key === "foo";
const isBar = (key: string): key is "bar" => key === "bar";
const validator = (key: string, secondKey: string) => {
if (isFoo(key) && isFooKey(secondKey)) {
return { key, secondKey }
}
if (isBar(key) && isBarKey(secondKey)) {
return { key, secondKey }
}
return false;
}
// Here comes where the issue arises
const someFunc = (key: string, nestedKey: string) => {
const validated = validator(key, nestedKey);
if (validated) {
return decoder[validated.key][validated.secondKey];
}
return null;
}

谁能向我解释为什么这不能以我应该的方式工作,这是打字稿的缺点还是我的推理或实现的问题?如果有人对我的问题有更好的解决方案,我很想听听!

这里的根本问题是validated是我一直所说的相关记录类型,而TypeScript并没有很好的支持。 问题是decoder[validated.key]validated.secondKey的类型都是联合类型;前者属于{ foo: string; bar: string; } | { foo: string; }型,后者属于"foo" | "bar"型。 TypeScript 的类型系统几乎无法表示它们之间存在相关性的事实。

通常,如果我有两个联合类型的值,其中每个联合类型有两个成员,如declare const x: A | B; declare const y: C | D;,则[x, y]的类型类似于[A, C] | [A, D] | [B, C] | [B, D]。 但是您碰巧知道,例如,如果xA型,那么y将是C型,反之亦然......因为x的类型与y的类型相关。所以[A, D][B, C]是不可能的。 因此[x, y]应该只[A, C] | [B, D]. 但是编译器无法自己推断出这一点,因此它抱怨这些不可能的情况。

在您的情况下,编译器无法验证decoder[validated.key][validated.secondKey]是否为有效的索引操作。 它认为decoder[validated.key]可能{ foo: string },而validated.secondKey可能"bar"。 所以它抱怨。


您可以采取一些措施来解决此问题。 一种是使用类型断言来告诉编译器不要担心。 这是最不安全的类型,但它不会更改运行时代码:

(decoder[validated.key] as { foo: string, bar: string })[validated.key]

您基本上已经声称decoder[validated.key]是其两种可能类型的交集,因此您可以安全地为其"foo" | "bar"属性编制索引。


您可以编写冗余代码来引导编译器完成两种可能性:

validated.key == "foo" ?
decoder[validated.key][validated.secondKey] :
decoder[validated.key][validated.secondKey]

在这里,它使用控制流分析将validated缩小到两种可能性,之后validated.keyvalidated.secondKey不再是联合类型。


这是处理相关记录的两种主要方法。 对于您的代码,第三种可能性本身就表明了:由于validator()函数实际上分别经历了这两种可能性,因此您可以将索引移动到该函数中以利用控制流分析:

const validatorAndDecoder = (key: string, secondKey: string) => {
if (isFoo(key) && isFooKey(secondKey)) {
return { key, secondKey, val: decoder[key][secondKey] }
}
if (isBar(key) && isBarKey(secondKey)) {
return { key, secondKey, val: decoder[key][secondKey] }
}
return null;
}
const someFunc2 = (key: string, nestedKey: string) => {
const validated = validatorAndDecoder(key, nestedKey);
if (validated) {
return validated.val
}
return null;
}

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

操场链接到代码

TL;博士

你可以这样做:

function pluck<T, K extends keyof T>(o: T, propertyName: K): T[K] {
return o[propertyName];
}
function validator(obj: typeof decoder, key: string, secondKey: string) {
if (isFoo(key) && isFooKey(secondKey)) {
const item = pluck(obj, key);
const subItem = pluck(item, secondKey);
return subItem;
}
if (isBar(key) && isBarKey(secondKey)) {
const item = pluck(obj, key);
const subItem = pluck(item, secondKey);
return subItem;
}
return null;
}

解释

索引类型

使用索引类型,可以让编译器检查使用动态属性名称的代码。

看这里 (pluck(

最新更新