Spread-operatoren i TypeScript garanterer ikke korrekt type
Jeg bruger spredeoperatoren til at oprette en kopi med et ændret felt som i det følgende eksempel (kør). Men compileren klager ikke, hvis typen af feltet ændres, når objektliteralet oprettes:
class A{
constructor(readonly a: number, readonly b: string[]){}
copy(): A { // must return an object of type A
return {
...this,
b: 99 // should be of type string[]!
}
}
}
const a: A = new A(1, ["x", "y"])
const a1 = a.copy()
console.log(JSON.stringify(a1))
Output :
[LOG]: "{"a":1,"b":99}"
At oprette en eksplicit grænseflade hjælper heller ikke:
interface IA {
readonly a: number
readonly b: string[]
}
...
copy(): IA { ...
Det ser ud til, at det ikke havde noget at gøre med arrays, siden det næste eksempel også kompilerer:
class A{
constructor(readonly a: number, readonly b: number){}
copy(): A {
return {
...this,
b: "my string" // should be of type number!
}
}
}
Hvorfor er dette muligt i TypeScript? Eller er det en compiler-fejl? Findes der en måde at undgå det på
Svar #1
Dette er en interaktion mellem følgende typer systemfakta:
thiser virkelig af den polymorfethis-type. Polymorfthiser en implicit generisk typeparameter af klassen, der udvider den aktuelle klasse. (reference se sidste afsnit)- Hvis spredningsoperationen involverer operander af en generisk type, er resultatet et snit af typerne af de elementer, der blev spredt. reference
- Et snit kan tildeles til nogen af dets bestanddele. Så for eksempel kan typen
A & Btildeles bådeAogBuanset eventuelle uforeneligheder mellem nogen af egenskaberne afAogB(eksempel) (reference se Tildelingskompatibilitet)
Ved at anvende disse 3 regler her får vi typen af { ...this, b: 99 } som this & { b: number } (bevis). Som kan tildeles A, da this extends A.
Hvis du for eksempel skulle foretage en typeassert this til A, ville du faktisk få en fejl, da spread-operationer, der ikke involverer generiske operander, er typer korrekt (fx)
Om dette er en fejl, vil jeg kalde det en designbegrænsning. I lang tid var det ikke muligt at bruge spread med generisk typede operander. Denne funktion blev tilføjet i 3.2 (PR), og når man læser noterne i PR, kan vi tydeligt se, at teamet er klar over nogle af hullerne her:
Et alternativ til at bruge snitpunkter ville være at indføre en højereordens typeoperator
{ ...T, ...U }på linje med det, som #10727 udforsker. Selvom dette i første omgang synes attraktivt, ville det kræve et betydeligt arbejde at implementere denne nye typekonstruktor og udstyre den med alle de funktioner, vi allerede implementerer for snittyper, og det ville give lille eller ingen præcisionsgevinst i de fleste scenarier. Især er forskellene faktisk kun vigtige, når spread-udtryk involverer objekter med overlappende egenskabsnavne, der har forskellige typer. Desuden ville typen{ ...T, ...T }for en ubegrænset typeparameterTfaktisk ikke kunne tildelesT, hvilket, selvom det teknisk er korrekt, ville være pedantisk irriterende.
Fremhævningen ovenfor blev tilføjet, dit brugsscenarie falder ind under dette præcise problem.
Svar #2
Jeg kender ikke den præcise forklaring, men grunden til, at dette sker, er typen af this.
class A {
constructor(readonly a: number, readonly b: string[]){}
copy(): A { // must return an object of type A
const obj = {
...this,
b: 99 // should be of type string[]!
}
return {
...this,
b: 99 // should be of type string[]!
}
}
}
I ovenstående kode er typen af this this i stedet for A. Så typen af objektet obj er -
this & {
b: number;
}
som også er typen af det returnerede objekt og på en eller anden måde kan tildeles til A (jeg kender ikke årsagen). Legeplads
Løsningen er - tilføj typen af dette i funktionssignaturen
class A {
constructor(readonly a: number, readonly b: string[]){}
copy(this: A): A { // must return an object of type A
// ^^^^^^^^^
const obj: A = {
...this,
b: 99 // should be of type string[]! // error
}
return {
...this,
b: 99 // error
}
}
}
Legeplads link
