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
