Typescript-objektnøgler fra generiske parametre
Jeg forsøger at oprette en funktion, der accepterer et array af objekter og derefter en eller flere parametre, der angiver de egenskaber, som vil blive brugt på det array.
For eksempel, enten af disse:
update([{ name: 'X', percentage: 1, value: 2 }], 'percentage', 'value');
update([{ name: 'X', foo: 1, bar: 2 }], 'foo', 'bar');
Forsøg 1:
function update<T extends { name: string }, A extends keyof T, B extends keyof T>(data: T[], a: A, b: B) {
for (const row of data) {
console.log(row.name, row[a] * row[b]);
row[a] = row[a] * row[b];
}
}
- Når jeg forsøger at bruge
row[a]som et tal, får jeg en fejl, fordi typen erT[A]og tilsyneladende ved den ikke, at det er et tal - Når jeg prøver at tildele et tal til
row[a], får jegType 'number' kan ikke tildeles typen 'T[A]'.(2322)
Forsøg 2:
type Entry<A extends string, B extends string> = { name: string } & Record<A | B, number>;
function update<T extends Entry<A, B>, A extends string, B extends string>(data: T[], a: A, b: B) {
for (const row of data) {
console.log(row.name, row[a] * row[b]);
row[a] = row[a] * row[b];
}
}
- Dette løser problemet, når man bruger
row[a], det ser ud til at være ok for mig at bruge det som et tal Men jeg får stadig
Type 'number' er ikke tildelbar til type 'T[A]'.(2322), når jeg prøver at tildele et tal tilrow[a](eller noget som helst for den sags skyld)- Overraskende nok løser det ikke problemet at fjerne
{ name: string }fraEntry, TypeScript tillader stadig af en eller anden grund ikke, at jeg tildeler noget her
- Overraskende nok løser det ikke problemet at fjerne
Dette virker som en ret almindelig ting at ville gøre i JS, men jeg har ingen idé om, hvordan man får Typescript til at forstå det.
Svar #1
Du har helt sikkert brug for noget som din Entry<A, B>-definition for at repræsentere noget, som man ved har en number-egenskab ved A og B-nøglerne. Bare det at sige, at A og B er keyof typeof data, er ikke nok, fordi compileren ikke ved, om der måske er ikke-numeriske egenskaber ved keyof typeof data (faktisk er name-egenskaben ikke-numerisk).
Desuden ønsker du ikke, at data skal være af generisk type T extends Entry<A, B>. Den begrænsning er en øvre grænse, så T kunne være noget meget specifikt som {name: string, pi: 3.14, e: 2.72} med egenskaber af number literals. Den eneste værdi, der kan tildeles typen 3.14, er 3.14, så compileren forhindrer korrekt, at du tildeler row[a] * row[b] til row[a]. Så vi vil bare have, at data er af typen Entry<A, B> og ikke en eller anden ukendt subtype af Entry<A, B>. Det giver os dette:
function update<A extends string, B extends string>(data: Entry<A, B>[], a: A, b: B) {
for (const row of data) {
console.log(row.name, row[a] * row[b]);
row[a] = row[a] * row[b]; // error! Type 'number' is not assignable to type 'Entry<A, B>[A]'
}
}
som stadig bryder på tildelingen. Kompilatoren kan ikke se, at number kan tildeles til Entry<A, B>[A]. Noget ved snittet forvirrer den. Jeg tror, dette er en begrænsning i TypeScript (selvom nogen kunne forsøge at argumentere for, at hvis A er "name", så er Entry<A, B>[A] af typen number & string, også kendt som never, og derfor har kompilatoren ret i at advare dig om, at number ikke nødvendigvis kan tildeles til Entry<A, B>[A], men dette er ret pedantisk og ikke særligt nyttigt i de fleste situationer, efter min mening). Jeg har ikke fundet et specifikt problem på GitHub, der taler om det.
På den anden side kan compileren se, at number kan tildeles Record<A, number>[A]. Da Entry<A, B> er en underordnet type af Record<A, number>, kan vi derefter sikkert udvide row til Record<A, number> (med forbehold for ovenstående "name"-pedanteri) ved at tildele det til en ny variabel med den passende typeannotering:
function update<A extends string, B extends string>(data: Entry<A, B>[], a: A, b: B) {
for (const row of data) {
console.log(row.name, row[a] * row[b]);
const r: Record<A, number> = row; // okay
r[a] = row[a] * row[b];
}
}
Og nu fungerer alt som ønsket:
update([{ name: 'X', percentage: 1, value: 2 }], 'percentage', 'value');
update([{ name: 'X', foo: 1, bar: 2 }], 'foo', 'bar');
Playground-link til kode
Svar #2
Du kan oprette en Mapped type (https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) for at håndtere talværdier, og derefter tilføje {name: string}. Desværre er du nødt til at caste multiplikationsresultatet til T[keyof T], da TS ikke kan finde ud af, at kun tal kan tildeles ikke-name felter.
update([{ name: 'X', percentage: 1, value: 2 }], 'percentage', 'value');
update([{ name: 'X', foo: 1, bar: 2 }], 'foo', 'bar');
type NumberValues<Type> = {
[Property in keyof Omit<Type, 'name'>]: number;
};
function update<T extends NumberValues<T> & {name: string}>(data: T[], a: keyof T, b: keyof T) {
for (const row of data) {
const aValue = row[a];
console.log(row.name, row[a] * row[b]);
row[a] = row[a] * row[b] as T[keyof T];
}
}
Derfra kan du ændre update-metoden til at acceptere flere parametre af typen keyof T som vist nedenfor:
function update<T extends NumberValues<T> & {name: string}>(data: T[], ...args: (keyof T)[]) {
for (const row of data) {
console.log(row.name, row[args[0]] * row[args[1]]);
row[args[0]] = row[args[0]] * row[args[1]] as T[keyof T];
}
}
Hvis du vil undgå casting, kan du gemme den udtrukne række i en variabel, som du kan typificere, på samme måde som i @jcalz-løsningen:
function update<T extends NumberValues<T> & {name: string}>(data: T[], ...args: (keyof T)[]) {
for (const row of data) {
const r: {[property in keyof T]: number} = row;
r[a] = row[a] * row[b];
}
}
Jeg er ikke sikker på, om du sigtede efter det, men den foreslåede løsning giver dig mulighed for at tilføje flere felter til objektet og sende ikke alle nøgler, så for eksempel:
update([{ name: 'X', percentage: 1, value: 2, anotherVal: 5 }], 'percentage', 'value');
vil også fungere.
