Tillåter Typescript mutationer som bryter typ-säkerhet?


I följande kodstycke visas det att Typescript inte kan varna för mutation av strikt typade objekt på ett sätt som bryter mot begränsningarna i dess typ?

type Animal = {
  name: 'cat'
  noise: 'meow'
} | {
  name: 'dog'
  noise: 'woof'
};


const f = (animal: Animal) => {
  // Why is this allowed? Typescript still considers this to be of type 'Animal'
  // after this mutation, even though its value no longer assignable to any variant
  // of Animal.
  animal.noise = 'woof';

  // ❌ - Typescript failed to update animal's type when we mutated it, so now
  // it mistakenly continues to believe it is assignable to Animal everywhere!
  // Now its incorrectly typed data can spread throughout the application,
  // breaking type safety, potentially everywhere...
  const anotherAnimal: Animal = animal;

  // ✅ - Typescript correctly prevents us from assigning this combination of fields
  // to type Animal. But this is exactly the value we mutated to in our first example,
  // so why were we allowed to do that when the outcome is the same, invalid result?
  const animalMatchingPostMutationValue: Animal = {
    name: 'cat',
    noise: 'woof'
  }
}

const animal: Animal = { 
  name: 'cat',
  noise: 'meow',
}

f(animal)

Se lekplatslänken här.

Har jag fel i att tro att detta borde förhindras? Om inte, finns det ett annat sätt än att helt enkelt undvika mutationer (kanske genom att använda readonly för att upprätthålla denna praxis)?

REDIGERA

Omarbetat exempel för att undvika typomvandling.

Svar #1

Du bör inte använda as Animal i

const animal: Animal = { 
  name: 'cat',
  noise: 'meow',
} as Animal

det är inte logiskt och gör din kod osäker.

Vänligen se dokumenten

Typantaganden är ett sätt att tala om för kompileraren "lita på mig, jag vet vad jag gör." Ett typantagande är som en typkonvertering i andra språk, men det utför ingen speciell kontroll eller omstrukturering av data. Det har ingen påverkan vid körning och används enbart av kompileraren. TypeScript förutsätter att du, programmeraren, har utfört alla speciella kontroller som du behöver.

När du har skrivit as Animal, antar TypeScript att vilken Animal som helst kan tilldelas lämplig egenskap:

const animal: Animal = {
  name: 'cat',
  noise: 'meow',
} as Animal

animal.name // "cat" | "dog"
animal.noise // "meow" | "woof"

Det kan inte längre avgöra om det är en del av facket eller inte.

type Name = Animal['name'] // "cat" | "dog"
type Noise = Animal['noise'] // "meow" | "woof"

Detta är anledningen till att typkonverteringar är osäkra och bör övervägas endast om du inte har något annat alternativ.

Mutationer är lite osäkra operationer i TypeScript eftersom TS inte spårar dem förutom i ett fall.

Du kan hitta mer information om mutationer i TypeScript i min artikel:

Sammanfattning

Försök bara att undvika mutationer i Typescript

Lista över svar relaterade till mutationer i TypeScript

Hur man selektivt tilldelar från en Partial till en annan i TypeScript,

Varför kan jag indexera med en sträng för att få ett egenskapsvärde men inte för att sätta det?

TypeScript: Varför kan jag inte tilldela ett giltigt fält i ett objekt med typen { a: "a", b: "b" }

Varför säger Typescript att denna variabel är "refererad direkt eller indirekt i sin egen initialiserare"?

https://github.com/microsoft/TypeScript/pull/30769

keyof och React Redux-åtgärd

Kan inte dynamiskt ställa in egenskapsvärden för gränssnittsobjekt

Konvertera Object.entries reduce till generisk typ

Sammanfattning Huvudproblemet är att objektets muterbara egenskaper är kovarianta. Betrakta detta exempel stulet från Titian-Cernicova-Dragomir 'stalk:

type Type = {
    name: string
}

type SubTypeA = Type & {
    salary: string
}

type SubTypeB = Type & {
    car: boolean
}

let employee: SubTypeA = {
    name: 'John Doe',
    salary: '1000$'
}

let human: Type = {
    name: 'Morgan Freeman'
}

let student: SubTypeB = {
    name: 'Will',
    car: true
}


// same direction
type Covariance<T> = {
    box: T
}

let employeeInBox: Covariance<SubTypeA> = {
    box: employee
}

let humanInBox: Covariance<Type> = {
    box: human
}

/**
 * MUTATION 
 */
let test: Covariance<Type> = employeeInBox

test.box = student // mutation of employeeInBox

const result_0 = employeeInBox.box.salary // while result_0 is undefined, it is infered a a string

console.log({ result_0 })

result_0 är undefined medan TS tror att det är en string. För att fixa det, använd bara readonly-flaggan för COvariance-typen:

type Covariance<T> = {
    readonly box: T
}

Nu misslyckas det som det borde.

Arbetslösning, utan readonly

type Cat = {
  name: 'cat'
  noise: 'meow'
}

type Dog = {
  name: 'dog'
  noise: 'woof'
};

type Animal<Name, Noise> = { name: Name, noise: Noise }

const f = <
  CatName extends Cat['name'],
  CatNoise extends Cat['noise'],
  DogName extends Dog['name'],
  DogNoise extends Dog['noise'],
  Pet extends Animal<CatName, CatNoise> | Animal<DogName, DogNoise>
>(animal: Pet) => {

  animal.noise = 'woof'; // error

  const anotherAnimal: Pet = animal;

  // error
  const animalMatchingPostMutationValue: Animal = {
    name: 'cat',
    noise: 'woof'
  }
}
const animal: Animal = {
  name: 'cat',
  noise: 'meow',
}

f(animal) // ok

Lekplats Du kan göra den kontravariant. Ser inte bra ut men fungerar.

Du kan använda eslint-plugin för att upprätthålla readonly-flaggor

Relaterade artiklar