undefined is not a function and
cannot access property x of undefined. The ability to loosely type variables and override them with ease can result in a debugging nightmare. ES6 helps to alleviate some of these concerns via block scoping variables and the introduction of const. However, that can only go so far. The addition of a static type system can further help remedy these problems.
Types allow developers to make impossible states impossible. There are times where a variable should only be one of a predetermined allowed set of values. Without a type system the developer may create a switch statement for the various expected variable paths and provide a default case for when someone passes an incorrect variant. Unfortunately, there is no inherited documentation that accompanies the function call site with allowed variants. The developer is then left to track down how the function is used, hope the documentation is up to date or engage in a trial and error pattern. All these options waste valuable time. A type system helps to correct this issue by allowing the appropriate use of enums to allow a variable to be one of several preselected options. If a value doesn’t match the enum, the compiler will throw an error.
Documenting functions and modules across a codebase can be difficult, especially when the function declaration and usage are in separate files. Earlier attempts such as JSDoc relied on comment block annotations. Component and module based architecture encourages storing reusable functionality in separate modules to allow for drier coder. Beyond the use of enums, a type system increases discoverability in calling functions or classes from other modules. Editors can integrate with a type system and provide tool tips documenting the function. These tooltips may describe what arguments a function accepts, what type they are, and what the function returns. The end result is readily available documentation of how to use a function at the call site. There are also benefits when looking at the function declaration. The type signature of the function allows for quick review of what it accepts and returns.
TLDR: type systems are free documentation.
Type systems reduce the need to create unit tests which check the structure of the data received. These unit tests are tedious and can require significant effort to update when the data structure needs change. Using tests to assert the structure of the data is as expected is using the wrong tool for the job. Type systems, by design, excel at declaring data structures and accompanying types. They are more efficient for declaring the shape of data, and flagging any errors if there are deviations from the type. If there are common data patterns throughout an application, the type declarations can often be removed to their own module and imported into other modules where needed.
TLDR: types systems are free unit tests.
Refactoring is where type systems shine. Similar to tests, a significant benefit of type systems comes from refactoring code and resolving errors which arise. If the developer changes a type, the compiler alerts them to all instances which use that type and now have mismatch errors. This is powerful to know where to go to resolve an issue, instead of manually checking hundreds or thousands of files.
For example, a primitive UI component may update the structure of the data that it expects to receive. Without a type system, the developer would need to search for all components which use this primitive and then update them to the new structure. If the change is significant, they may also want to manually check the various changes to confirm they are working as intended. A type system will flag all instances where the primitive is now called incorrectly. This allows the developer to know exactly which files to update, and have a greater sense of confidence that the issues have been resolved once the type error is cleared.
TLDR: type systems help us refactor with confidence.
Handling potential null and undefined values can be overlooked and the cause of many runtime errors, such as the ones mentioned above. Type systems help to alleviate this problem, by flagging if there is an unhandled case where a value may be null or undefined. For example, if an object may not exist and the developer tries to access a property on the object, a type system will throw an error. To resolve the error, the code must check that the object exists. This process helps prevent the runtime error of
cannot access property x of undefined.