所以你想要一个函数dottedKVsToObj(),它接受一个KV对象数组,定义为
interface KV {
key: string, // <-- dotted path
value: any
}
并返回一个对象,其结构对应于将value 属性放置在由虚线路径字符串key 表示的路径中。
如果你不关心这个函数的输出类型(除了像 Record<string, unknown> 这样非常宽泛的东西),一种写法是这样的:
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 对,我们将split 和key 放入一个路径元素数组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<T> 的对象,该对象是根据自身定义的。我不打算在这里详细介绍它是如何工作的,尽管可以根据要求回答任何特别的问题。
让我们看看它是如何工作的:
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<string, boolean>,即真的不值得努力。
无论如何,既然你并不真正关心这一点,我不会担心边缘情况、详细解释或优化人们如何称呼它。
Playground link to code