映射到对象的元组数组不会产生正确的值
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]
中没有 0
或 1
索引,因此错误是正确的。很多时候编译器无法验证这些索引是否安全,即使它们是安全的,所以你可能最终会回到 EntryKey
和 EntryValue
解决方案,它使用 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;
} */
我的一个朋友试图编写一个类型来进行类似于 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]
中没有 0
或 1
索引,因此错误是正确的。很多时候编译器无法验证这些索引是否安全,即使它们是安全的,所以你可能最终会回到 EntryKey
和 EntryValue
解决方案,它使用 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;
} */