类型安全的通用 reduce util 从数组创建对象
Type-safe generic reduce util to create object from array
我正在寻找一种通用且类型安全的方法来在 TypeScript 中对以下 JavaScript 进行建模:
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' ] }
// }
我们的想法是创建一个实用程序,它将一个数组缩减为一个对象,该对象由给定键的值作为键控,并且具有整个对象的值,或者可以选择给定第二个键的特定数据点(这个解释可能有点差,但希望这个例子不言自明)。
这是非常简单的 JS,但似乎很难在 TS 中获得正确的类型。到目前为止,这是我想出的,但我需要创建两个函数才能使 return 类型正确,如果感觉有点老套的话。我无法获得条件 return 类型在这里工作,所以如果必须这样的话,我可以使用两个函数,但想知道这里是否有更好的方法(可能会导致 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]>);
}
P.S。我知道 lodash 有一个类似的 keyBy
功能,但我不认为它们有任何类似于上面显示的 keyByWith
的功能。
最大的问题是 records
被推断为类型:
{
name: string;
id: number;
data: string[];
}[]
也就是说keyBy(records, 'name')
只能还给你string
。如果您将 as const
断言添加到 records
,那么您可以获得一些文字字符串,并且您可以使用更强大的类型。
const records = [
{ name: "foo", id: 1, data: ["foo"] },
{ name: "bar", id: 2, data: ["bar"] },
{ name: "baz", id: 3, data: ["baz"] }
] as const;
然后您需要将 reduce
的结果对象键入
Record<T[K] & ObjectKey, T>
或
Record<T[K] & ObjectKey, T[K2]>
以便使用通用 T
中的密钥。
具有无效键类型的 T[K] & ObjectKey
将解析为 never
,但您也会在那里抛出运行时异常,所以这无关紧要。
最后,您可以使用重载来声明多个签名来实现这一功能。这将有两个签名:
// 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"]
根据 Alex 的回答,实际上可以使用映射的对象类型完全推断出这种类型并正确区分它。但它肯定更冗长,需要一些按摩。
const testA = keyBy(records, "name");
testA.foo.data // readonly ["foo"]
const testB = keyBy(records, "name", "data");
testB.foo // readonly ["foo"]
我继续使用其他答案中的一些工具来实现这个
//
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]
} {...}
上查看此内容
这不完全是我问题的答案,因为它是一种完全不同的方法,但我一直在研究这个问题,认为值得分享使用回调的同一问题的替代解决方案,不是键,以提取结果对象的键和值。
这种方法与@AlexWayne 接受的答案具有相同级别的类型安全性,尽管不如@CodyDuong 的那么多。
但是,它在对象值的数据转换方面支持更大的灵活性(而不是限于 T
或 T[K]
),并且不需要运行时检查来确保密钥对象的是一个有效的对象键(如果提供了一个无效的键键提取器,它只会编译失败):
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)])
);
我正在寻找一种通用且类型安全的方法来在 TypeScript 中对以下 JavaScript 进行建模:
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' ] }
// }
我们的想法是创建一个实用程序,它将一个数组缩减为一个对象,该对象由给定键的值作为键控,并且具有整个对象的值,或者可以选择给定第二个键的特定数据点(这个解释可能有点差,但希望这个例子不言自明)。
这是非常简单的 JS,但似乎很难在 TS 中获得正确的类型。到目前为止,这是我想出的,但我需要创建两个函数才能使 return 类型正确,如果感觉有点老套的话。我无法获得条件 return 类型在这里工作,所以如果必须这样的话,我可以使用两个函数,但想知道这里是否有更好的方法(可能会导致 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]>);
}
P.S。我知道 lodash 有一个类似的 keyBy
功能,但我不认为它们有任何类似于上面显示的 keyByWith
的功能。
最大的问题是 records
被推断为类型:
{
name: string;
id: number;
data: string[];
}[]
也就是说keyBy(records, 'name')
只能还给你string
。如果您将 as const
断言添加到 records
,那么您可以获得一些文字字符串,并且您可以使用更强大的类型。
const records = [
{ name: "foo", id: 1, data: ["foo"] },
{ name: "bar", id: 2, data: ["bar"] },
{ name: "baz", id: 3, data: ["baz"] }
] as const;
然后您需要将 reduce
的结果对象键入
Record<T[K] & ObjectKey, T>
或
Record<T[K] & ObjectKey, T[K2]>
以便使用通用 T
中的密钥。
具有无效键类型的 T[K] & ObjectKey
将解析为 never
,但您也会在那里抛出运行时异常,所以这无关紧要。
最后,您可以使用重载来声明多个签名来实现这一功能。这将有两个签名:
// 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"]
根据 Alex 的回答,实际上可以使用映射的对象类型完全推断出这种类型并正确区分它。但它肯定更冗长,需要一些按摩。
const testA = keyBy(records, "name");
testA.foo.data // readonly ["foo"]
const testB = keyBy(records, "name", "data");
testB.foo // readonly ["foo"]
我继续使用其他答案中的一些工具来实现这个
//
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]
} {...}
上查看此内容
这不完全是我问题的答案,因为它是一种完全不同的方法,但我一直在研究这个问题,认为值得分享使用回调的同一问题的替代解决方案,不是键,以提取结果对象的键和值。
这种方法与@AlexWayne 接受的答案具有相同级别的类型安全性,尽管不如@CodyDuong 的那么多。
但是,它在对象值的数据转换方面支持更大的灵活性(而不是限于 T
或 T[K]
),并且不需要运行时检查来确保密钥对象的是一个有效的对象键(如果提供了一个无效的键键提取器,它只会编译失败):
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)])
);