[英]Type-safe generic reduce util to create object from array
我正在寻找一种通用且类型安全的方式来 model 以下 JavaScript in TypeScript:
const records = [
{ name: "foo", id: 1, data: ["foo"] },
{ name: "bar", id: 2, data: ["bar"] },
{ name: "baz", id: 3, data: ["baz"] }
];
function keyBy(collection, k1, k2) {
if (k2) {
return collection.reduce((acc, curr) =>
({ ...acc, [curr[k1]]: curr[k2] }), {});
} else {
return collection.reduce((acc, curr) =>
({ ...acc, [curr[k1]]: curr }), {});
}
}
console.log(keyBy(records, "name", "data"));
// { foo: [ 'foo' ], bar: [ 'bar' ], baz: [ 'baz' ] }
console.log(keyBy(records, "name"));
// {
// foo: { name: 'foo', id: 1, data: [ 'foo' ] },
// bar: { name: 'bar', id: 2, data: [ 'bar' ] },
// baz: { name: 'baz', id: 3, data: [ 'baz' ] }
// }
这个想法是创建一个 util,它将一个数组缩减为 object,该数组由给定键的值作为键控,并且值为整个 object,或者可选地为给定第二个键的特定数据点(此解释可能是有点差,但希望这个例子不言自明)。
这是非常简单的 JS,但似乎很难在 TS 中获得正确的类型。 到目前为止,这是我想出的,但我需要创建两个函数来获得正确的返回类型,如果一切都感觉有点老套的话。 我无法在这里获得条件返回类型,所以如果必须这样的话,我可以使用两个函数,但想知道这里是否有更好的方法(可能会导致Record<T[K], T>
或Record<T[K], T[K2]>
而不是由ObjectKey
键控的记录)。 谢谢。
type ObjectKey = string | number | symbol;
const isValidKey = (x: any): x is ObjectKey =>
typeof x === "string" || typeof x === "number" || typeof x === "symbol";
function keyBy<T extends object, K extends keyof T>(collection: T[], key: K) {
return collection.reduce((acc, curr) => {
const valueAtKey = curr[key];
if (isValidKey(valueAtKey)) {
return { ...acc, [valueAtKey]: curr };
}
throw new Error("T[K] is not a valid object key type");
}, {} as Record<KeyType, T>);
}
function keyByWith<T extends object, K extends keyof T, K2 extends keyof T>(
collection: T[],
k: K,
k2: K2,
) {
return collection.reduce((acc, curr) => {
const valueAtKey = curr[k];
if (isValidKey(valueAtKey)) {
return { ...acc, [valueAtKey]: curr[k2] };
}
throw new Error("T[K] is not a valid object key type");
}, {} as Record<ObjectKey, T[K2]>);
}
PS 我知道 lodash 有一个类似的keyBy
function,但我不认为他们有任何类似于上面显示的keyByWith
的东西。
最大的问题是records
被推断为类型:
{
name: string;
id: number;
data: string[];
}[]
这意味着keyBy(records, 'name')
只能给你返回string
。 如果您向records
添加一个as const
断言,那么您可以获得一些文字字符串,并且您可以使用更强大的类型。
const records = [
{ name: "foo", id: 1, data: ["foo"] },
{ name: "bar", id: 2, data: ["bar"] },
{ name: "baz", id: 3, data: ["baz"] }
] as const;
然后你需要输入你的reduce
结果 object 作为
Record<T[K] & ObjectKey, T>
或者
Record<T[K] & ObjectKey, T[K2]>
以便使用通用T
中的键。
具有无效键类型的T[K] & ObjectKey
将解析为never
,但您还会在那里抛出运行时异常,因此这无关紧要。
最后,您可以使用重载来声明多个签名,使这个成为 function。这将有两个签名:
// One key
function keyBy<
T extends object,
K extends keyof T
>(
collection: readonly T[],
key: K
): Record<T[K] & ObjectKey, T>
// Two keys
function keyBy<
T extends object,
K extends keyof T,
K2 extends keyof T
>(
collection: readonly T[],
k: K,
k2: K2,
): Record<T[K] & ObjectKey, T[K2]>
以及类似的实现:
// Implementation
function keyBy<
T extends object,
K extends keyof T,
K2 extends keyof T
>(
collection: readonly T[],
k: K,
k2?: K2,
): Record<T[K] & ObjectKey, T[K2]> | Record<T[K] & ObjectKey, T> {
return collection.reduce((acc, curr) => {
const valueAtKey = curr[k];
if (isValidKey(valueAtKey)) {
if (k2) return { ...acc, [valueAtKey]: curr[k2] };
return { ...acc, [valueAtKey]: curr };
}
throw new Error("T[K] is not a valid object key type");
}, {} as Record<T[K] & ObjectKey, T[K2]> | Record<T[K] & ObjectKey, T>);
}
现在这有效:
const testA = keyBy(records, "name");
testA.foo.data // readonly ["foo"] | readonly ["bar"] | readonly ["baz"]
const testB = keyBy(records, "name", "data");
testB.foo // readonly ["foo"] | readonly ["bar"] | readonly ["baz"]
根据亚历克斯的回答,实际上可以使用映射的 object 类型完全推断出这种类型并正确区分它。 但它肯定更冗长,需要一些按摩。
const testA = keyBy(records, "name");
testA.foo.data // readonly ["foo"]
const testB = keyBy(records, "name", "data");
testB.foo // readonly ["foo"]
我继续并从其他答案中获取了一些工具来实现这一目标
//https://stackoverflow.com/questions/61410242/is-it-possible-to-exclude-an-empty-object-from-a-union
type AtLeastOne<T, U = {[K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
type ExcludeEmpty<T> = T extends AtLeastOne<T> ? T : never;
function keyBy<
T extends Record<any, any>,
K extends keyof T,
K2 extends keyof T
>(
collection: readonly T[],
k: K,
): {
[P in T[K]]: ExcludeEmpty<{
[P2 in keyof T as T[K] extends P ? T[K] : never]: T
}>[P]
}
function keyBy<
T extends Record<any, any>,
K extends keyof T,
K2 extends keyof T
>(
collection: readonly T[],
k: K,
k2: K2,
): {
[P in T[K]]: ExcludeEmpty<{
[P2 in keyof T as T[K] extends P ? T[K] : never]: T[K2]
}>[P]
}
// Implementation
function keyBy<T extends Record<any, any>, K extends keyof T, K2 extends keyof T>(
collection: readonly T[],
k: K,
k2?: K2,
): {
[P in T[K]]: ExcludeEmpty<{
[P2 in keyof T as T[K] extends P ? T[K] : never]: T
}>[P]
} | {
[P in T[K]]: ExcludeEmpty<{
[P2 in keyof T as T[K] extends P ? T[K] : never]: T[K2]
}>[P]
} {...}
在TS 游乐场上查看
这不完全是我问题的答案,因为它是一种完全不同的方法,但我一直在研究这个问题,并认为值得分享一个替代解决方案来解决使用回调而不是键来提取的同一问题结果 object 的键和值。
这种方法与@AlexWayne 接受的答案具有相同级别的类型安全性,尽管不如@CodyDuong 的那么多。
但是,它在对象值的数据转换方面支持更大的灵活性(而不是限于T
或T[K]
),并且不需要运行时检查来确保 object 的密钥是有效的 object 密钥(而不是如果提供了无效的密钥密钥提取器,它只会编译失败):
type User = {
id: number;
name: string;
data: string[] | undefined;
};
const records: User[] = [
{ id: 1, name: "foo", data: ["fee"] },
{ id: 2, name: "baz", data: ["fi"] },
{ id: 3, name: "bar", data: undefined },
];
type ObjectKey = string | number | symbol;
type ToKey<T extends object, U extends ObjectKey> = (
value: T,
index: number
) => U;
type ToValue<T extends object, V> = (value: T, index: number, arr: T[]) => V;
function keyBy<T extends object, K extends ObjectKey>(
collection: T[],
keyExtractor: ToKey<T, K>
): Record<K, T>;
function keyBy<T extends object, K extends ObjectKey, V>(
collection: T[],
keyExtractor: ToKey<T, K>,
valueExtractor?: ToValue<T, V>
): Record<K, V>;
function keyBy<T extends object, K extends ObjectKey, V>(
collection: T[],
keyExtractor: ToKey<T, K>,
valueExtractor?: ToValue<T, V>
) {
return collection.reduce<Record<K, T> | Record<K, V>>(
(acc, curr, index, arr) => ({
...acc,
[keyExtractor(curr, index)]: valueExtractor
? valueExtractor(curr, index, arr)
: curr,
}),
{} as any
);
}
const nameToData = keyBy(
records,
(x) => x.name,
(x) => x.data
);
const nameToUser = keyBy(records, (x) => x.name);
const indexToUser = keyBy(records, (x, i) => i);
const indexToName = keyBy(
records,
(x, i) => i,
(x) => x.name
);
const idToTransformedData = keyBy(
records,
(x, i) => i,
(x) => x.data?.map((s) => [s.repeat(3)])
);
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.