Hur man ställer in nyckeltyp och värdetyp vid objekt tilldelning i TypeScript
interface Student {
id: number
name: string
age: number
}
let alice: Student = { name: "alice", age: 19, id: 1 }
const bob: Student = { name: "Bob", age: 19, id: 2 }
const keys: (keyof Student)[] = ["age", "name"]
for (let c of keys) {
alice[c] = bob[c] // here shows the error during compile
/*
src/index.ts:11:3 - error TS2322: Type 'string | number' is not assignable to type 'never'.
Type 'string' is not assignable to type 'never'.
*/
}
Hur redigerar man koden så att inget TypeScript-fel uppstår under alice[c]=bob[c]?
Svar #1
För närvarande, när du skriver kod av formen alice[c]=bob[c], analyserar kompilatorn bara typerna av nycklarna och inte deras identiteter. Det betyder att den inte kan avgöra skillnaden mellan alice[c]=bob[c] och alice[c1]=bob[c2] där c1 och c2 båda är av samma typ som c. Man kan tro att detta är okej och att båda sidorna av tilldelningen är av typen för indexerad åtkomst Student[keyof Student].
Men TypeScript behandlar indexerad åtkomst skrivningar annorlunda när nyckeln är en unionstyp som keyof Student. Denna säkerhetsfunktion implementerades i microsoft/TypeScript#30769, och den fångar faktiskt legitima problem, eftersom alice[c1]=alice[c2] verkligen inte är säker; vad händer om c1 är "id" men c2 är "name", till exempel?
Men det förbjuder också viss helt säker kod. För alice[c]=bob[c] vet vi att c på varje sida inte bara är av samma typ, utan att det är exakt samma värde. Det finns en funktionförfrågan om att stödja denna typ av "samma-nyckel" tilldelning på microsoft/TypeScript#32693, men den har ännu inte implementerats.
Tills och om det inte implementeras, kommer du behöva refaktorera till en version där kompilatorn ser de två sidorna av tilldelningen som identiska eller kompatibla. Enligt en kommentar på microsoft/TypeScript#30769 kan du få detta om k är av en generisk typ.
Därför är det enklaste tillvägagångssättet här att lägga tilldelningen inuti en generisk funktion... och det enklaste sättet att göra det är förmodligen att byta från en for...of-loop till forEach()-arraymetoden, som tar en callback:
keys.forEach(<K extends keyof Student>(c: K) => {
alice[c] = bob[c]
});
Nu är c av den generiska typen K, och båda sidor av tilldelningen ses som den generiska indexerade typåtkomsttypen Student[K], vilket är tillåtet.
Playground-länk till kod
Svar #2
c i for-loopen har typen keyof Student, dvs. kan vara vilken nyckel som helst i Student, varifrån typen på både alice[c] och bob[c] är unionen av alla egenskapstyper i Student, nämligen string | number.
Och problemet är att när du försöker göra tilldelningen alice[c]=bob[c], är TS inte smart nog att veta att alice[c] och bob[c] måste vara av samma typ: TS vet bara string | number, vilket sedan är typen på RHS, men på LHS blir det (är ekvivalent med) string & number (unionen blir en snitt!), vilket i sin tur förenklas till never. Därav felet.
Här är kod som fungerar, inte nödvändigtvis bra för dig beroende på hur "generisk" du faktiskt behöver vara, men den illustrerar det faktum att om alla egenskaper hade samma typ, skulle tilldelningen fungera alldeles utmärkt, så typkontroller sådana att det i varje villkorsgren finns en känd unik egenskapstyp är tillräckliga för att få det att fungera:
interface Student {
id: number
name: string
age: number
}
let alice: Student = { name: "alice", age: 19, id: 1 };
const bob: Student = { name: "Bob", age: 19, id: 2 };
declare const keys: (keyof Student)[];
// to show that results do not depend on the specific value of `keys`
for (const c of keys) {
if (c === "name") {
c; // const c: "name"
alice[c] = bob[c]; // no error
}
else {
c; // const c: "id" | "age"
alice[c] = bob[c]; // no error
}
c; // const c: keyof Student
const a_ = alice[c]; // const a_: string | number
const b_ = bob[c]; // const b_: string | number
alice[c] = bob[c]; // ERROR
// Type 'string | number' is not assignable to type 'never'.
}
type TEST = string & number; // type TEST = never
Länk till lekplats
