Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why TypeScript gradual typing should stop compiling this valid code?

Tags:

typescript

In the following code, although everything looks good but TypeScript shows an error which seems strange:

class Sample {

    private value = 1;

    private incrementValue(): void {
        this.value++; 
    }

    private beginTest(): void {

        if (this.value !== 1) {
            throw "bad state!"
        }

        this.incrementValue();

        if (this.value != 2) {
            throw "bad state!"; // ERROR!!: This condition will always return 'true' since the types '1' and '2' have no overlap.
        }
    }
}

Why should TypeScript raise such error? I'm changing value by this.incrementValue(). It seems this kind of error should be an more of an IntelliSense Warning rather than a Compile Error.

Possibly it is because of the gradual type system, but as it fails it these situations there should be a compile option to workaround this.

Is there any workaround or special compile option to turn this off?

like image 227
mehrandvd Avatar asked Oct 15 '22 14:10

mehrandvd


1 Answers

While TypeScript's type inference is usually very good at inferring types correctly, the language has limited control flow analysis. The lead developer on the TypeScript team at Microsoft argues:

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.

— Ryan Cavanaugh, Trade-offs in Control Flow Analysis

Explanation of your example

In the first if-statement TypeScript evaluates this.value as a number type.

Right after the first if-statement, TypeScript determines that this.value is no longer of the type number. Instead it is of the literal type 1.

This is because that any value that is different from 1 (!== 1) would have terminated the function. Therefore, any value that doesn't terminate and instead moves on to the next if-statement, must be the exact literal value 1.

When you call this.incrementValue();, TypeScript isn't aware that the function mutates the state of this.value. By the time you get to the 2nd if statement, TypeScript still believes that this.value is of the literal type 1.

Because of this, it evaluates the 2nd if-statement the same way it would if you had replaced this.value with the number literal 1

if (1 != 2)

(This also throws the exact same error)

Solutions for your problem

As for your problem right now, the simplest solution would be to either:

  • Tell TypeScript that this.value is a number if ((this.value as number) != 2)
  • Cast the value to a number if (+this.value != 2)
  • Add a // @ts-ignore comment on the line above so the TypeScript compiler ignores the error

Solutions for improving TypeScript's control flow analysis

Ryan Cavanaugh mentions a few ideas for improving TypeScript's control flow analysis:

  • pure modifier on functions that says this function doesn't modify anything. This is a bit impractical as we'd realistically want this on the vast majority of all functions, and it doesn't really solve the problem since lots of functions only modify one thing so you'd really want to say "pure except for m"

  • volatile property modifier that says this "this property will change without notice". We're not C++ and it's perhaps unclear where you'd apply this and where you wouldn't.

like image 153
Daniel Avatar answered Oct 21 '22 07:10

Daniel