typescriptcastingtype-inference

typescript automatically inferring type of a union indirectly


Here is the example which is self-explementary:

type TypeX = {t:"a", a1:string} | {t:"b", a2: string};
let t: TypeX | undefined;
function setA(){
    t = {t:"a", a1:"values"};
}
setA();
console.log(t.a1);

TS complians on last line:

Property 'a1' does not exist on type 'TypeX'. Property 'a1' does not exist on type '{ t: "b"; a2: string; }'.(2339)

Other than casting t to {t:"a", a1:string}, is there ay other way to tell TS, hey I know the type is {t:"a", a1:string} because setA is just called.

I am looking for something like, "hey TS, when funcation setA is called, the t type is {t:"a", a1:string}".

BTW, I know I can do runtime check like the following, but I am looking for more a compile time check and a bit smarter than an additional if:

if(t && t.t === "a"){
    console.log(t.a1);
}

Solution

  • TS does some control-flow analysis, but does not inline function calls.

    When seeing a function call, it has to decide what effect it has on local variables. Currently, it does not even attempt any analysis, and assumes that there are no side effects.

    See Trade-offs in Control Flow Analysis #9998

    The primary question is: When a function is invoked, what should we assume its side effects are?

    One option is to be pessimistic and reset all narrowings, assuming that any function might mutate any object it could possibly get its hands on. Another option is to be optimistic and assume the function doesn't modify any state. Both of these seem to be bad.

    This problem spans both locals (which might be subject to some "closed over or not" analysis) and object fields.

    Current approach is optimistic, which results in missing narrowing in your example, and runtime error in example below:

    type TypeX = {t:"a", a1:string} | {t:"b", a2: string};
    let t: TypeX | undefined = {t:"a", a1:"values"};
    
    function setA(){
      t = undefined;
    }
    
    setA();
    console.log(t.a1);
    

    TS cannot realistically inline all function calls and analyze resulting paths - and currently it does not even try. Function calls can explode in complexity - functions can call other functions, call themselves recursively etc. In general you cannot even prove that a function will terminate.

    Currently there are no mechanisms to annotate a function's effect on closed-over variables. The closest "annotate side effect" feature is called an assertion function, but that only tracks side effects of things passed to the function, not closed-over variables.

    There are some proposals to enable flow control analysis for immediately-invoked functions like Annotate immediately-invoked functions for inlining flow control analysis #11498