Uncaught TypeError: Cannot read property 'value' of undefined
This cryptic message is just telling us that the variable we are reading does not point to an object but to
undefined. Finding the cause of these errors is fairly simple if the application is small enough, but it becomes exponentially more complicated as the codebase grows.
- Type correctness checking, such as:
- Access to null values
- Missing fields on objects
- Not implemented edge cases
- Real time type-based code Autocompletion:
- Strong integration with IDEs like Visual Studio Code
- Allows to have always updated and correct documentation, since it's based directly on the types in the code
In this article, however, we won't talk about the language specifically, since it already has an excellent documentation and many detailed guides. To show the real value of this tool we'd like instead to focus on our experience, and all the times TypeScript really helped us to solve certain problems.
🧨 1. Access to null values
A first example comes directly from our blog, during the development of the internationalization.
The first time a user visits the website their preferred language is saved in LocalStorage. When the user visits the root of the website again, they're redirected to the correct language path (
/en for example), using a table that contains the path for every language.
This wrong implementation works fine, until someone removes a language from the website or corrupts the saved preference.
Reading the language's
baseUrl field, if
languageKey is not present in languages we will read an
undefined field. This causes a
TypeError: languages[languageKey] is undefined exception, that will crash React and the entire application.
If we analyze the code with TypeScript, keeping it as-is without adding any type annotation, we will get che following error:
Typescript was able to spot the mistake without any additional type annotation, saving us from a probable crash (try it yourself on the Playground).
Being able to verify accesses to null fields makes TypeScript even more powerful and secure than other compilers, such as Java, where
null dereference is referred as the "million dollar mistake".
⏱ 2. Typing numerical values: time units management
Another hard problem when developing software is time management. Dealing with durations, timestamps and timezones requires a lot of attention, since it can easily lead to off-by-one errors or wrong units.
In SMC, we built a web application to manage employees timetables. We receive an employee's information from a service, including their working hours, saved as minutes in JSON format.
In this case, TypeScript interprets
shiftHours values type as
number. This may sound harmless, but it doesn't allow us to verify that they are represented as minutes, not as hours. Unless you remark it in the documentation, another developer could easily make a mistake and swap the unit of measure, interpreting that number as hours.
Using this React component that display the information for an employee we won't have errors or exceptions in runtime, and in fact TypeScript considers it as correct.
The problem with this implementation is that we are subsuming (a strange word worth learning!) everything as
number type is completely generic, and therefore the compiler does not enforce the correctness of the units of measurement. However, thanks to TypeScript, we can avoid this type of logical errors by using a pattern called Nominal Typing. This technique involves adding more strict annotations to basic types, such as numbers or strings, allowing to type check their content as well.
A first solution would be to use a type alias. We soon realize, however, that it does not work as we would expect:
This way a developer who use the
HourDisplay component won't even notice in the autocompletion that the expected type is in hours (
Hours), since the aliases are removed for clarity.
The code above compiles without errors, since TypeScript uses Structural Typing, not Nominal Typing. This means that checking whether two types are compatible is done with their content, not their name. This behavior may seem strange if a developer comes from languages like Java or C#, in fact it comes from functional languages like Haskell or OCaml.
To solve this problem and check the types in a nominal way there are many possible approaches, both runtime based and compile-time only. However, all these patterns have in common adding a tag field to an object that acts as discriminant.
The standard solution, used by TypeScript compiler developers themselves, is the following, based on a
void tag present only at compile-time:
This pattern is very useful not only when managing time units, but also in many other cases. For example, we have used this technique on some projects to manage multiple identifiers types, adding nominal types to strings. This allowed us to save a lot of testing time given the complexity of the domain.
For more information about Nominal Typing, I recommend reading this article and this library.
3. 📌 Constant size arrays: XYZ coordinates
In SMC we love 3D graphics, especially on the Web. But when using many different libraries like Three.JS and react-three-fiber, not all of them handle the coordinates in the same way.
While some handles them as objects with x, y and z, in other libraries we find the coordinates represented as a three-element array
[x, y, z]:
When some functions expect coordinates as arrays and other as objects, it's very easy to make mistakes, producing errors in the console that are not immediately understood:
Fortunately, TypeScript was designed specifically to find this kind of errors, and type-checking the code immediately finds the error. This is because Three.JS has typings that allow the compiler to verify the correctness of the function parameters.
However things becomes slightly more complicated when we want to use arrays to represent coordinates. While TypeScript can easily verify the presence of the
z fields of an object, when we access an array with the notation
arr [N] we may read values that are out of bounds.
In fact, TypeScript does not perform bounds checking on arrays, since it depends on run-time information. All arrays represented as
T have no compile-time bounds checking, and literal arrays are interpreted this way by default.
We can have better checking using Type Narrowing:
As we forced our type to be a three number array, we can also do the same with different types:
However, if our data is not a constant, for example if it comes from a service or from a file, we should not cast it using
as, as it would be unsafe: we don't know what data we are going to get. To get even more security we can combine a runtime control with a Type Guard:
We have seen how TypeScript can become a valid ally if you want to write safe and correct code, minimizing errors on edge cases and time needed for tests.
Some might argue that everything we showed could be solved with proper testing or more attention. In SMC, however, we think that the value that TypeScript brings is really this: it decreases the cognitive load that developers have to use when writing code at the small cost of a little more verbosity. The types act as a reference and help thanks to the IDE, so we no longer have to look for the parameters of a function in the documentation (which is often not even updated).
In SMC we applied the use of TypeScript to every complex and mission-critical projects, obtaining a decrease in the time required for bugfixes, as much less bugs are found in the testing phase.