You should not use as Animal
in
const animal: Animal = {
name: 'cat',
noise: 'meow',
} as Animal
it doe not make sense and makes your code unsafe
.
Please see the docs
Type assertions are a way to tell the compiler “trust me, I know what I’m doing.” A type assertion is like a type cast in other languages, but it performs no special checking or restructuring of data. It has no runtime impact and is used purely by the compiler. TypeScript assumes that you, the programmer, have performed any special checks that you need.
Once you have written as Animal
, TypeScript assumes that any Animal
is assignable to appropriate property:
const animal: Animal = {
name: 'cat',
noise: 'meow',
} as Animal
animal.name // "cat" | "dog"
animal.noise // "meow" | "woof"
It is no more able to distinguish is it a part of union or not.
type Name = Animal['name'] // "cat" | "dog"
type Noise = Animal['noise'] // "meow" | "woof"
This is why type assertions are unsafe and should be considered only if you don't have any other option.
Mutations are a bit unsafe operations in typescript because TS does not track them except one case.
You can find more information about mutations in typescript in my article:
TL;DR
Just try to avoid mutations in typescript
List of answers related to mutations in typescript
How to selectively assign from one Partial to another in typescript,
Why can I index by string to get a property value but not to set it?
TypeScript: Why can't I assign a valid field of an object with type { a: "a", b: "b" }
Why does Typescript say this variable is "referenced directly or indirectly in its own initializer"?
https://github.com/microsoft/TypeScript/pull/30769
keyof and React Redux action
Cannot dynamically set interface object's property values
Convert Object.entries reduce to Generics type
Summary
The main problem is that object mutable properties are covariant. COnsider this example stolen from 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
is undefined
whereas TS think that it is a string
. In order to fix it, just use readonly
flag for COvariance
type:
type Covariance<T> = {
readonly box: T
}
Now it fails as it should be.
Workaround, without 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
Playground
You can make it contravariant. Does not look nice but works.
You can use eslint plugin to enforce readonly
flags