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:

  1. this er virkelig af den polymorfe this-type. Polymorf this er en implicit generisk typeparameter af klassen, der udvider den aktuelle klasse. (reference se sidste afsnit)
  2. Hvis spredningsoperationen involverer operander af en generisk type, er resultatet et snit af typerne af de elementer, der blev spredt. reference
  3. Et snit kan tildeles til nogen af dets bestanddele. Så for eksempel kan typen A & B tildeles både A og B uanset eventuelle uforeneligheder mellem nogen af egenskaberne af A og B (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 typeparameter T faktisk ikke kunne tildeles T, 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

Relaterede artikler