- Compiler does not understand control flow in closure / callback function.
- In flow based type analysis, copmiler will be either optimistic or pessimistic. TypeScript is optimistic.
- You can usually workaround (3) by using
This is a long due introduction for TypeScript’s flow sensitive typing (also known as control flow based type analysis) since its 2.0 release. It is so unfortunate that no official documentation is in TypeScript’s website for it (while both flow and kotlin have!). But if you dig the issue list earnestly enough, you will always find some hidden gems there!
To put it short, a variable’s type in a flow sensitive type system can change according to the control flow like
while. For example, you can dynamically check the truthiness of a nullable variable and if it isn’t null, compiler will automatically cast the variable type to non-null. Sweet?
Without knowing what
makeSideEffect is, we cannot guarantee variable
a is still
number. Side effect can be as innocuous and innocent as
console.log('the number of life', 42), or as evil as a billion dollar mistake like
a = null, or even a control-flow entangler:
throw new Error('code unreachale').
One might ask compiler to infer what
makeSideEffect does since we can provide the source of the function.
However this is not practically feasible because of ambient function and (possibly polymorphic) recursion. Compiler will be trapped in infinite loops if we instruct it to infer arbitrary deep functions, as halting problem per se.
So a realistic compiler must guess what a function does by a consistent strategy. Naturally we have two alternatives:
- Assume every function does not have relevant side effect: e.g. assignment like
a = null. We call this optimistic.
- Assume every function does have side effect. We call this strategy pessimistic.
Spoiler: TypeScript uses optimistic strategy.
We will walk through these two strategies and see how they work in practice.
But before that let’s see some common gotchas in flow sensitive typing.
Flow sensitive typing does not play well with callback functions or closures. This is explicitly mentioned in Kotlin’s document.
var local variables - if the variable is not modified between the check and the usage and is not captured in a lambda that modifies it;
Consider the following example.
As a developer, you can easily figure out that
string will be output to console because
setTimeout will call its function argument asynchronously, after assigning
a. Unfortunately, this knowledge is not accessible to compiler. No keyword will tell compiler whether callback function will be called immediately, nor static analysis will tell the behavior of a function:
forEach is the same in the view of compiler.
So the following example will not compile.
Note: compiler will still inline control flow analysis for IIFE(Immediately Invoked Function Expression).
In the future, we might have a keyword like
immediate to help compiler reasoning more about control flow. But that’s a different story.
Now let’s review the strategies for function call.
Optimistic flow typing assume a function without side-effect that changes a variable’s type. TypeScript chooses this strategy in its implementation.
This assumption usually works well if code observes immutable rule. On the other hand, a stateful program will be tolled with the tax of explicit casting. One typical example is scanner in a compiler. (Both Angular template compiler and TypeScript itself are victims).
Such bad behavior also occurs on fields.
Optimistic flow analysis sometimes is also unsound: a compiler verified program will cause runtime error. We can easily construct a function which reassigns a variable to an object of different type and uses it as of the original type, and thus a runtime error!
The above examples might leave to you a impression that compiler does much bad when doing optimistic control flow inference. In practice, however, a well architected program with disciplined control of side effect will not suffer much from compiler’s naive optimistism. Presumption of immutability innocence will save you a lot type casting or variable rebinding found in pessimistic flow sensitive typing.
A pessimistic flow analysis places burden of typing proof on programmers.
Every function call will invalidate previous control flow based narrowing. (Pessimistic possibly has a negative connotation, conservative may be a better word here). Thus programmers have to re-prove variable types is matching with previous control flow analysis.
Examples in this section are crafted to be runnable under both TS and flow-type checker.
Note, only flow-type checker will produce error because flow is more pessimistic/strict than TypeScript.
Alas, pessimistism also breaks fields. Example taken from stackoverflow.
These false alarms root in the same problem as in optimistic strategy: compiler/checker has no knowledge about a function’s side effect. To work with a pessimistic compiler, one has to assert/check repeatedly so to guarantee no runtime error will occur. Indeed, this is a trade-off between runtime safety and code bloat.
Sadly, no known panacea for flow sensitive typing. We can mitigate the problem by introducing more immutability.
const identifier will never change its type.
const will provide you runtime safety or bypass pessimistic checker.
The same should apply to
readonly, but current TypeScript does not seem to support it.
Flow sensitive typing is an advanced type system feature. Working with control flow analysis smoothly requires programmers to control mutation effectively.
Keeping mutation control in your mind. Flow sensitive typing will not in your way but make a pathway to a safer code base!