【问题标题】:Make Json Object with properties coming from array of objects in typescript使用来自打字稿中的对象数组的属性制作 Json 对象
【发布时间】:2021-11-08 23:11:18
【问题描述】:

我有一个对象数组

  [
   {"key": "key1", "value" : true },
   {"key": "key2.subKey1", "value" : true },
   {"key": "key2.subKey2", "value" : true },
  ]

我如何获得一个对象,该对象从键字符串中获取参数并在 typescript 中进行类似的操作?

{
 "key1" : true,
 "key2" : {
   "subkey1" : true,
   "subkey2" : true
  }
}

我知道您可以使用(点)添加一个值。但是我如何使用包含所有内容的字符串并做到这一点?因为特别是在打字稿中你不能我认为(我是打字稿的新手)

【问题讨论】:

  • this 是您要找的吗?您是否试图让它在 type 级别(即编译器确切地知道输出的类型)以及 value 级别(即对象你在运行时得到)?它们基本上是两个独立的问题;前者需要使用模板文字类型进行 TypeScript 类型操作;后者可以在纯 JavaScript 中完成。您还有其他可以测试的用例吗?我很高兴能写出答案,但我不确定我对问题的理解程度是否足够好。
  • 嗨!因此,基本上我确实从 JSON 文件开始,该文件与问题中的文件类似,然后您可以说的“路径”被保存在一个对象中,以便可以保存在一个数据库中。现在的问题是从数据库中获取数据并重建初始 JSON 文件。我并不真正关心类型,因为它被声明为 Record,所以即使 key 的值是一个对象也没关系
  • 这正是我想要的,谢谢!

标签: javascript node.js reactjs angularjs typescript


【解决方案1】:

所以你想要一个函数dottedKVsToObj(),它接受一个KV对象数组,定义为

interface KV {
    key: string, // <-- dotted path
    value: any
}

并返回一个对象,其结构对应于将value 属性放置在由虚线路径字符串key 表示的路径中。


如果你不关心这个函数的输出类型(除了像 Record&lt;string, unknown&gt; 这样非常宽泛的东西),一种写法是这样的:

function dottedKVsToObj(kvs: readonly KV[]) {
    const ret: Record<string, unknown> = {}
    for (const { key, value } of kvs) {
        let cur: any = ret;
        const path = key.split(".");
        const lastKey = path.pop()!;
        for (const k of path) {
            if (!(k in cur)) cur[k] = {};
            cur = cur[k];
        }
        cur[lastKey] = value;
    }
    return ret;
}

您可能可以通过函数式编程技术而不是显式循环来做到这一点,但我发现这样考虑更容易。对于kvs 输入中的每个key/value 对,我们将splitkey 放入一个路径元素数组path。这将始终包含至少一个元素,因此我们将数组中的最后一个元素pop 用作最终键,它将获得value。然后,我们获取我们的返回对象ret,并开始根据path 中的路径元素进入属性和子属性。如果我们那里还没有对象,我们用一个空对象来初始化它。

您可以验证这是否适用于有问题的示例输入:

const kvs = [
    { "key": "key1", "value": true },
    { "key": "key2.subKey1", "value": true },
    { "key": "key2.subKey2", "value": true },
];
    
const obj = dottedKVsToObj(kvs);
console.log(obj)
/* {
  "key1": true,
  "key2": {
    "subKey1": true,
    "subKey2": true
  }
} */

如果您确实关心函数的输出类型,则可以使用相同的实现,但为调用签名提供 generic 函数类型,该函数类型使用涉及 template literal types 的递归类型来拆分 literal 路径类型在每个点字符处建立一个与输出对象对应的对象类型。它比实现要脆弱和复杂得多,并且可能存在一堆边缘情况。

为了好玩,这里有一种方法:

type Prefix<T> = T extends `${infer A}.${infer R}` ? A : T;

type RemoveKeyPrefix<T extends KV> =
    T extends { key: `${infer A}.${infer R}`, value: infer V } ? { key: R, value: V } : never;

type DottedKVsToObj<T extends { key: string, value: any }> = { [E in T as Prefix<E['key']>]:
    RemoveKeyPrefix<E> extends infer F ?
    [F] extends [never] ? E['value'] : DottedKVsToObj<Extract<F, KV>> : never
} extends infer O ? { [K in keyof O]: O[K] } : never;

declare function dottedKVsToObj<T extends KV>(kvs: readonly T[]): DottedKVsToObj<T>;

因此,函数dottedKVsToObj 将一个T 对象数组限制为KV,并返回一个类型为DottedKVsToObj&lt;T&gt; 的对象,该对象是根据自身定义的。我不打算在这里详细介绍它是如何工作的,尽管可以根据要求回答任何特别的问题。

让我们看看它是如何工作的:

const kvs = [
    { "key": "key1", "value": true },
    { "key": "key2.subKey1", "value": true },
    { "key": "key2.subKey2", "value": true },
] as const;
    
const obj = dottedKVsToObj(kvs);
/* const obj: {
    key1: true;
    key2: {
        subKey1: true;
        subKey2: true;
    };
} */

看起来不错。 obj 的推断类型与实现返回的实际值相匹配。请注意kvs 的定义需要const assertion,这样编译器就不会将"key2.subKey1" 之类的键扩展为string...如果发生这种情况,您能得到的最好的就是Record&lt;string, boolean&gt;,即真的不值得努力。

无论如何,既然你并不真正关心这一点,我不会担心边缘情况、详细解释或优化人们如何称呼它。

Playground link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-01-04
    • 2023-03-15
    • 1970-01-01
    • 2018-12-31
    • 1970-01-01
    • 2021-12-17
    • 2023-03-07
    • 1970-01-01
    相关资源
    最近更新 更多