Understand TypeScript’s Control Flow Based Type Analysis

Marius Schulz
InstructorMarius Schulz

Share this video with your friends

Send Tweet
Published 5 years ago
Updated 3 years ago

The TypeScript compiler analyzes the control flow of your program to determine the most precise type for a variable at any given location.

This lesson shows control flow based type analysis in action and explains how type guards and jumps in control flow affect the type that is inferred.

In this lesson, I want to look at how TypeScript analyses the control flow of your program to give you better type information. Here, we have our trim and lower function from the previous lesson again. I'm going to start off by adding a dummy text statement in various places of this function so that we find out the exact type of text in all of these places.

In line two text is of type string or null or undefined. This shouldn't be too surprising, because after all this is exactly the type that we specified in our type annotation here. In line four, text is inferred to be a string. This is because we have added a type guard here and we've check the type actually is, well, a string.

Here is where it gets interesting. In this position text is inferred to be of type null or undefined. The string type has been removed from the union type. This is because TypeScript has figured out that if text did contain a string we would have already left the function in line five.

If we make it after this if statement, so if we make it to line seven, it is not possible that text contains a string value, so string can be excluded from the union type and what remains is null and undefined.

This sort of flow analysis is really powerful, because it understands more than just plain type guards using the type of operator. We could have also written if text to exclude all falsy values. In this case text would still be typed as a string within the if statement.

In line seven however, text is now typed as string or null or undefined. Notice that the type string is included in this union type, and this is because text could still contain the empty string, in which case we would not have left the function in line five because we would not have entered the if statement.

The flow analysis also understands Boolean negations. We could also add an early return statement here and return if the text was falsy and only do the trimming and lower casing otherwise. Within the if block text is typed as string, or null, or undefined, and after the if block the only possibility for text is to be of type string. If text were null or undefined we would have left the function in line five.

If I now remove all the dummy text statements, we get a clean implementation of the trim and lower function that is fully statically typed. We could go one step further and refactor the function body to use a conditional expression and the ternary operator. Even then, flow analysis would infer the correct type of text within both expressions.

Flow analysis gives rise to another feature called definite assignment analysis. Take this variable declaration, for example. We're declaring a variable called foo that is of type number. Implicitly, this variable now contains the value undefined, because we have not initialized it or assigned anything to it.

If we now try to print foo to the console we get a type error saying that we're reading the value of foo before anything was assigned to it. If we add an assignment to foo before we use it the type checker is happy and the error goes away.

Alternatively, we could also explicitly allow the value undefined in the variable declaration. Note that all of this only works if you've turned on strict null checks in your compiler configuration. Flow analysis doesn't just assign a type to a variable in a given scope, it really analyses the flow of your program.

If we assign a number to foo here and then hover over the variable name, we can see that foo is now inferred to be of type number. Then if we want to, we can dot into foo and access all the methods to find on the number prototype.

If we assign undefined later we can no longer do anything afterwards. If we say foo dot we don't get any auto completion here. Now, the type is inferred to be undefined.