I have the following toy example where typescript warns me of an error:
type obj = {
ok: "ok" | "error",
}
function main(a: obj){
a.ok = "ok";
reloadFromDatabase(a);
if (a.ok == "error"){ // TS error
console.log("Error");
}
}
// Any function that has side effects on object a.
// For example, typeorm's `a.reload()`
function reloadFromDatabase(a: obj){
a.ok = "error";
}
The error is:
This comparison appears to be unintentional because the types '"ok"' and '"error"' have no overlap.(2367)
It makes sense if you assume side effect that mutate objects are not allowed, but the type of a
is obj
so it should allow both values ("ok" | "error"
) for obj.isOk
.
In this particular code example where I have my hands on the function that has side effects its fine because I could just return the mutated object:
function main(a: obj){
a.ok = "ok";
a = reloadFromDatabase(a);
if (a.ok == "error"){ // This time its fine
console.log("Error");
}
}
function reloadFromDatabase(a: obj){
a.ok = "error";
return a;
}
But in my usecase I don't have access to the reloadFromDatabase
and it returns nothing (it just mutate the object).
Is that a typescript option? Does it seems like a legitimate error? Am I missing a case where allowing mutation would completely break typescript logic (offer less/no type safety)?
This is part of the trade-offs made in control flow analysis:
You can have a look at this famous issue : https://github.com/microsoft/TypeScript/issues/9998
It can summed up as the question :
The primary question is: When a function is invoked, what should we assume its side effects are?
And right now, TS assume there are no side effects.
The work around for this is to use a type assertion :
if (a.ok as obj['ok'] == "error") { // ok asserted as 'ok'|'error'
console.log("Error");
}