This lesson introduces the --strictNullChecks
compiler option and explains how non-nullable types differ from nullable types. It also illustrates how you can write safer code by being explicit about null
and undefined
in the type system.
Let's say you want to write a TypeScript function that takes a text as a parameter and returns a trimmed and lower-cased version of that text. You could use this function to normalize a host URL, for example. I've already set up tsconfig.json file, so we can go ahead right away and compile this project.
From the disc folder, we now see the generated JavaScript file, which looks pretty much exactly like our TypeScript file except that the type annotation is gone. If we run this file we should get the expected output.
Now, what happens if we pass null to our function? Our code is still type correct. However, if we compile the project again and then run the resulting JavaScript file we get an error saying that we cannot read the property trim of null. Why was this code type correct in the first place if it's going to blow up at runtime anyway?
The problem is that in TypeScript null and undefined can be assigned to any type, so we can assign some string value to a string but we can also assign null and undefined at any time and the type checker will not complain about it. This is true for all types in TypeScript, including other primitive types such as numbers and Booleans.
Luckily, there is a compiler option that lets us change this behavior. We are going to head over to the tsconfig.json file and we're going to turn on the strict null checks compiler option. If we now head back to our TypeScript file we get a bunch of type errors.
This is because in strict null checking mode null and undefined are no longer valid values of any type. Now the type checker complains that type null is not assignable to type string, and the same goes for undefined.
All of these types are now non nullable types. If we want to allow the text variable to contain the value null or undefined we can use a union type and include the new type null to allow the assignment of null, and we can also include the new type undefined to include the undefined value.
Now, we no longer get a type error for the text variable. We can do the same thing with a num variable, and say that it's either a number or the value null but nothing else. Which is why the attempt to assign undefined is not type correct.
All right, let's see how we can use the null and undefined types to fix our trim and lower function. Our function should accept a string, the value null, and the value undefined. Let's add three test calls here passing a string, a value null, and a value undefined. We now have to change our type annotation to include the new types null and undefined as well.
Our three function calls are now type correct, but we also get a new type error in line two saying that text is possibly null or undefined. The problem here is that we're calling the trim method on a value that could potentially be null or undefined, in which case the JavaScript engine throws an error at runtime.
TypeScript forces us to check the type of the text parameter before we try to access any string members on it. We can do this by adding a type guard using the type of operator. If text is not a string we simply return text unchanged.
Now, all of our code type checks correctly. Notice that our type annotation up here is the only TypeScript specific syntax that we're using in this entire file. If we now compile the project one more time and then head over to the JavaScript file you'll that it looks pretty much identical to the TypeScript file, except of course for the type annotation and some white space.
If we now execute the code everything should work as expected. Being explicit about null and undefined in the type system can really help you understand how exactly an API works. For example, let's take document.getElementById.
Let's say you want to find the DOM element that has the ID app container and you want to store this element in a container variable. If you hover over the variable name you will see that container is either an HTML element or null. This already answers the question what document.getElementById if the element cannot be found.
Another advantage is that TypeScript will not let you dot into the container variable and do something. Visual Studio code does not give you any auto completion here. This is because we cannot do anything with container until we know that it's not null.
What we have do here is add a check, like this for example, to make sure that container is not null. Within here we can then access members to find an HTML element. There is one last feature I want to show you, and that is the non-null assertion operator which is written as an exclamation mark.
It looks exactly like the Boolean negation operator, although it's entirely different and has nothing to do with Booleans. In fact, this is TypeScript specific syntax, so it's not even plain JavaScript.
By adding the non-null assertion operator after the getElementById call we're telling the TypeScript compiler to assume that getElementById will not return null or undefined in this case.
This is why container is null typed as HTML element and no longer as HTML element or null as it was before. Therefore, we can dot into container into whatever we want without a preceding check.
Let's compile this project one last time. As you can see the type assertion has been compiled away. If document.getElementById actually does return null because the DOM element cannot be found, the call to remove in line two will result in a runtime error.
I suggest you treat the non-null assertion operator as an escape hatch and avoid it whenever possible.