typescriptmutationside-effects

Does typescript always assumes side effects are not allowed?


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)?


Solution

  • 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");
    }