我有一个对象类型数组,我希望将它转换为每个对象中的一个属性键控的对象。在概念上类似于Lodash的KeyBy,但在类型上。
type Arr = [
{slug: 'test-1', value: string},
{slug: 'test-2', value: string},
{slug: 'test-3', value: string},
]
type KeyBy<A extends object[], S extends keyof A[number]> = {
//1. how do I get numeric indexes of A for iteration and mapping?
//2. how do I convert those numeric indexes to the literal types of the matching property?
}
type Keyed = KeyBy<Arr, 'slug'>
/* desired result:
{
'test-1': {slug: 'test-1', value: string},
'test-2': {slug: 'test-2', value: string},
'test-3': {slug: 'test-3', value: string},
}
*/我认为我在KeyBy类型上设置的泛型约束是适当的,但它不需要适合这种约束。
发布于 2021-09-22 18:01:05
是的,这绝对是可能的
type KeyedBy<A, S extends PropertyKey> = A extends Array<infer T>
? KeyedByInternal<T, S>
: never;
type KeyedByInternal<T, S extends PropertyKey> = S extends keyof T ? {
[K in T[S] as Extends<K, PropertyKey>]: Extends<T, Record<S, K>>
} : never;
type Extends<T1, T2> = T1 extends T2 ? T1 : never;这将使您得到您想要的所有行为,既可以用于产生文字类型的选择器,也可以用于生成“宇宙”类型的选择器(例如,string或number):
// Tests
type Arr = [
{slug: 'test-1', value: string, id: 1},
{slug: 'test-2', value: string, id: 2},
{slug: 'test-3', value: string, id: 3},
]
type Keyed = KeyedBy<Arr, 'slug'>
/*
{
"test-3": {
slug: 'test-3';
value: string;
id: 3;
};
"test-1": {
slug: 'test-1';
value: string;
id: 1;
};
"test-2": {
slug: 'test-2';
value: string;
id: 2;
};
}
*/
type Keyed2 = KeyedBy<Arr, 'id'>
/*
{
3: {
slug: 'test-3';
value: string;
id: 3;
};
1: {
slug: 'test-1';
value: string;
id: 1;
};
2: {
slug: 'test-2';
value: string;
id: 2;
};
}
*/
// IT WORKS FOR NON-LITERAL TYPES TOO!
type Keyed3 = KeyedBy<Arr, 'value'>
/*
{
[x: string]: {
slug: 'test-1';
value: string;
id: 1;
} | {
slug: 'test-2';
value: string;
id: 2;
} | {
slug: 'test-3';
value: string;
id: 3;
};
}
*/真正的洞察力在于,我们可以使用Record<S, K> (其中S是表示Selector的文字类型,K是当前我们在映射类型中计算的键)过滤我们的联合类型的右侧,以便将值集中到我们处理的与选择器匹配的联合类型的分支(Es)。
发布于 2021-09-22 22:47:28
在这方面花费了更多的时间之后,我有了一个用数字索引解决这个问题的可行解决方案,虽然不像@Sean的解决方案那么健壮,因为它不支持通用类型,但它可能更容易理解,至少对我来说是这样。
/* helpers */
type IndecesOfArr<Arr extends any[]> = Exclude<Partial<Arr>['length'], Arr['length'] | undefined>;
type Extends<T1, T2> = T1 extends T2 ? T1 : never;
type Arr = [
{slug: 'test-1', value: string},
{slug: 'test-1', value: string, test: number}, //matching keys would be unioned
{slug: 'test-2', value: string},
{slug: 'test-3', value: string},
{slug: ['non-indexable'], value: string}, //should be excluded from result
]
type KeyBy<A extends object[], S extends keyof A[number]> = {
[I in IndecesOfArr<A> as Extends<A[I][S], PropertyKey>]: A[I]
}
type Keyed = KeyBy<Arr, 'slug'>
/* expected result:
{
'test-1': {slug: 'test-1', value: string}
| { slug: 'test-1', value: string, test: number},
'test-2': {slug: 'test-2', value: string},
'test-3': {slug: 'test-3', value: string},
}
*/IndecesOfArr类型的工作是因为类型记录对Arr['length']的处理。当给定一个文字数组时,它实际上告诉您文字长度,而不是只告诉number。如果将Arr包装在Partial<Arr>中,它将告诉您所有可能的长度(0长度)为一个联合。我们不包括全长数,以得到所有长度的零基并。
在映射索引中,我们利用这些索引,并将索引转换为带有该索引的S属性值值的键。然后,我们将该键分配给原始数组中该索引的值。
https://stackoverflow.com/questions/69288220
复制相似问题