typescript - Typ-säker generisk reduce-verktyg för att skapa objekt från array
Jag letar efter ett generiskt och typ-säkert sätt att modellera följande JavaScript i 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' ] }
// }
Idén är att skapa ett verktyg som kommer att reducera en array till ett objekt med nycklar baserade på värdet vid en given nyckel, och med ett värde som antingen är hela objektet, eller valfritt en specifik datapunkt vid en given andra nyckel (denna förklaring kan vara lite dålig, men förhoppningsvis talar exemplet för sig självt).
Det här är ganska enkel JS, men det verkar svårt att få typerna rätt i TS. Här är vad jag har kommit fram till hittills, men jag har behövt skapa två funktioner för att få returtyperna rätt och allt känns lite hackigt. Jag kunde inte få en villkorlig returtyp att fungera här, så jag är okej med två funktioner om det är så det måste vara, men undrar om det finns ett bättre tillvägagångssätt här (kanske något som skulle kunna resultera i Record<T[K], T> eller Record<T[K], T[K2]> snarare än att recordet är indexerat med ObjectKey). Tack.
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. Jag vet att lodash har en liknande keyBy-funktion, men jag tror inte att de har något som liknar keyByWith som visas ovan.
Svar #1
Det största problemet är att records tolkas som typen:
{
name: string;
id: number;
data: string[];
}[]
Vilket betyder att keyBy(records, 'name') endast kan ge dig tillbaka string. Om du lägger till en as const-assertion till records, kan du då få några bokstavliga strängar och du får starkare typer att arbeta med.
const records = [
{ name: "foo", id: 1, data: ["foo"] },
{ name: "bar", id: 2, data: ["bar"] },
{ name: "baz", id: 3, data: ["baz"] }
] as const;
Då behöver du skriva ut ditt reduce'ade resultatobjekt som
Record<T[K] & ObjectKey, T>
eller
Record<T[K] & ObjectKey, T[K2]>
så att nycklarna från den generiska T används.
Den T[K] & ObjectKey med en ogiltig nyckeltyp kommer att lösas till never, men du kommer också att generera ett runtime-undantag där så det spelar inte så stor roll.
Och slutligen kan du använda överlagring för att deklarera flera signaturer för att göra detta till en funktion. Detta kommer att ha två signaturer:
// 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]>
Och en implementering med något som:
// 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>);
}
Och nu fungerar detta:
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"]
Lekplats
Svar #2
Bygger vidare på Alex svar är det faktiskt möjligt att helt härleda denna typ och diskriminera den korrekt, genom att använda mappade objekttyper. Men det är definitivt mer ordrikt och kräver viss justering.
const testA = keyBy(records, "name");
testA.foo.data // readonly ["foo"]
const testB = keyBy(records, "name", "data");
testB.foo // readonly ["foo"]
Jag gick vidare och tog några verktyg från andra svar för att uppnå detta
//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]
} {...}
Visa detta på TS Playground
