[英]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.