我正在尋找一種通用且型別安全的方法來在 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 中獲得正確的型別。到目前為止,這是我想出的,但我需要創建兩個函式來獲得正確的回傳型別,如果一切都感覺有點 hacky。我無法讓條件回傳型別在這里作業,所以如果必須這樣的話,可以使用兩個函式,但想知道這里是否有更好的方法(可能會導致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功能,但我不認為他們有任何類似于keyByWith上面顯示的東西。
uj5u.com熱心網友回復:
最大的問題是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"]
操場
uj5u.com熱心網友回復:
建立亞歷克斯的答案,實際上可以使用映射物件型別來完全推斷這種型別并正確區分它。但它肯定更冗長,需要一些按摩。
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 Playground上查看
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/456087.html
