有没有一种方法可以为Typescript中的嵌套键访问创建类型保护?

时间:2020-02-10 16:39:57

标签: javascript typescript types

我正在重新设计以前使用香草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;
}

任何人都可以向我解释为什么这不符合我的预期,这是打字稿的缺点还是我的推理或实现的问题?如果有人对我的问题有更好的解决方案,我很想听听!

2 个答案:

答案 0 :(得分:1)

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

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

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


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

(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]

此处使用control flow analysisvalidated缩小为两种可能性,此后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;
}

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

Playground link to code

答案 1 :(得分:0)

TL; DR

您可以这样做:

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;
}

说明

索引类型

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

请参见herepluck