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

Mapped array of tuples to object doesn't produce correct values

我的一个朋友试图编写一个类型来进行类似于 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 开始)不支持 mapping array and tuple types while also remapping the keys. (See 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]> 是所有键的完整 union,而 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 解决方案,它使用 conditional type inference via infer 来回避这些问题。


对于你的第三个问题,你不能直接遍历 keyof E,这就是问题所在。不过,您可以做类似的事情,例如使用 the Exclude<T, U> utility type 显式过滤掉所有类似数组的属性:

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

Playground link to code