将元组映射到对象的数组不会生成正确的值



我的一个朋友正试图编写一个类型,该类型进行类似于Object.fromEntries的运行时行为的转换;即将两个元素元组的数组的类型(比如[["a", number], ["b", string]])转换为具有这些类型作为键值对的对象的类型(在这种情况下为{a: number; b: string})。

他第一次尝试这样做是在下面,但没有成功:

type ObjectKey = string | number;
type EntryKey<T> = T extends [infer K, unknown]
? K extends ObjectKey
? K
: never
: never;
type EntryValue<T> = T extends [ObjectKey, infer V] ? V : never;
type ObjFromEntriesBad<Entries extends [ObjectKey, unknown][]> = {
[Index in keyof Entries as EntryKey<Entries[Index]>]: EntryValue<
Entries[Index]
>;
};
// Incorrect: is `{a: string | number; b: string | number;}
type Test1 = ObjFromEntriesBad<[["a", number], ["b", string]]>

我写了一个简化版本,我认为它应该有效,但由于编译器错误而失败:

// Error: Type '0' cannot be used to index type 'Entries[Index]'
type ObjFromEntriesBad2<Entries extends [ObjectKey, unknown][]> = {
[Index in keyof Entries as Entries[Index][0]]: Entries[Index][1]
};

我终于想出了一个似乎有效的解决方案:

type ObjFromEntriesGood<Entries extends [ObjectKey, unknown][]> = {
[Tup in Entries[number] as Tup[0]]: Tup[1]
};

我的问题是:为什么第一个解决方案不起作用?为什么第二种解决方案会导致编译器错误?最后,在工作解决方案中,我们迭代Entries[number],但有没有一种方法可以创建正确的类型,同时仍然在映射类型中迭代keyof Entries

原始版本不起作用,因为目前(无论如何,从TS4.4开始)不支持在重新映射键的同时映射数组和元组类型。(请参阅microsoft/TypeScript#405886,以获得更改此项的建议。)一旦尝试,映射的对象就会获得数组的所有键,包括"push""pop"number:

type Foo<T> = { [K in keyof T as Extract<K, string | number>]: T[K] }
type Okay = Foo<{ a: 1, b: 2, c: 3 }> // same
type StillOkay = Foo<{ 0: 1, 1: 2, 2: 3 }> // same
type Oops = Foo<[1, 2, 3]>
/* type Oops = {
[x: number]: 1 | 2 | 3;
0: 1;
1: 2;
2: 3;
length: 3;
toString: () => string;
toLocaleString: () => string;
pop: () => 1 | 2 | 3 | undefined;
push: (...items: (1 | 2 | 3)[]) => number;
concat: {
(...items: ConcatArray<1 | 2 | 3>[]): (1 | ... 1 more ... | 3)[];
(...items: (1 | ... 2 more ... | ConcatArray<...>)[]): (1 | ... 1 more ... | 3)[];
};
... 23 more ...;
includes: (searchElement: 1 | ... 1 more ... | 3, fromIndex?: number | undefined) => boolean;
} */

所以在ObjFromEntriesBad:中

type ObjFromEntriesBad<E extends [ObjectKey, unknown][]> = {
[I in keyof E as EntryKey<E[I]>]: EntryValue<E[I]>;
};

您将得到number作为I in keyof E之一,EntryKey<E[I]>是所有键的完全并集,EntryValue<E[I]>是值的完全并合,您将失去所关心的相关性。

并且在ObjFromEntriesBad2中:

type ObjFromEntriesBad2<E extends [ObjectKey, unknown][]> = {
[I in keyof E as E[I][0]]: E[I][1]
};

这个问题仍然存在。当I类似于"push"时,E[I]中没有01索引,因此错误是正确的。很多时候,编译器无法验证这些索引是否安全,即使它们是安全的,所以您可能最终会回到EntryKeyEntryValue解决方案,该解决方案通过infer使用条件类型推断来回避这些问题。


对于您的第三个问题,您不能直接在keyof E上迭代,这就是整个问题。不过,您也可以做类似的事情,比如使用Exclude<T, U>实用程序类型显式过滤掉所有类似数组的属性:

type ObjFromEntries3<E extends [ObjectKey, unknown][]> = {
[I in Exclude<keyof E, keyof any[]> as EntryKey<E[I]>]: EntryValue<E[I]>;
};

并验证它是否真的有效:

type Test3 = ObjFromEntries3<[["a", number], ["b", string]]>
/* type Test3 = {
a: number;
b: string;
} */

游乐场链接到代码

最新更新