Written by William Lim✏️
Introduction
Before I start the discussion of over-engineering in TypeScript, I want to give a basic overview of how JavaScript and more strongly-typed languages currently deal with types. All types in JavaScript (with the exception of the object
type) are called primitive values
. Primitive values that are used a lot in JavaScript, include values with the type of string
, number
, and boolean
. All primitive values are immutable. For example, for strings, this means that once you have done the assignment let testString = ‘test’
, you cannot mutate it by doing something like testString[0] = ‘r’
to get rest
. You have to re-assign it, testString = ‘rest’
.
JavaScript is a loosely typed language. A variable in JavaScript can be reassigned to a value of any type such as number
, string
, or object
. For example, you can do testString = 4
. Notice how the string is now a number and the name of the variable is still testString
.
In contrast, C# can be an example of a more strongly-typed language. To explain this, let me give you an example. This is how you assign a string to a variable in C# string testString = “test”;
. Instead of using the var
or let
keyword, we declare the type of the variable, (string
), right when we assign it. Then, if we try to reassign it to a value of a different type, for example, by doing it like this testString = 4;
, you get the error error CS0029: Cannot implicitly convert type ‘int’ to ‘string’
:
testString
will always be a string. I want to note here that you should try to avoid adding types to the names of your variables, because, as you have seen in the loosely-typed language case, it gives the wrong type (testString
became a number), and in more strongly-typed languages, sometimes the type is already shown very clearly.
TypeScript is a language that allows JavaScript applications to have features that more strongly-typed languages often have. You can also choose how strict you want type checking to be and how much of your existing JavaScript code you want to migrate to TypeScript. You can make type checking very strict and do a full migration over to TypeScript, or you can even add TypeScript to your JavaScript projects to prepare for the future and not use it at all. In this article, I will go through the advantages and disadvantages of using TypeScript. This will give us a better idea of when to avoid over-engineering in TypeScript.
Applications built with more JavaScript
Before I start explaining the advantages and disadvantages of using TypeScript, I feel that I should start with an explanation of the advantages of not using it. For the purpose of keeping the length of this article not too long, I am only going to go through the primary way client-side scripting is done on the web in general — which is by using JavaScript. Discussion of related languages such as CoffeeScript or Elm, will not be included in this article. I will also explain the use of more JavaScript than TypeScript in server-side code.
One advantage of using more JavaScript is the speed that it brings. JavaScript does not have to be compiled and you can see results immediately. It does not have to be configured — you don’t have to create a configuration file (tsconfig.json
) and then add and alter its configuration. No extra configuration and no compile time means that you can prototype applications easily to see how your designs, data-fetching, and other features will turn out.
Another advantage of using more JavaScript is its maturity. It has been, at least in the last few years, the primary way to do scripting on the client-side/frontend. Sometimes, it is even used in the backend (Node.js). Because it has been so mature, it already includes ways of handling types depending on the library used. In Mongoose, for example, you can define schemas using code that looks like this: const userSchema = new Schema({ firstName: String })
. firstName
here is defined as having a String
type. Also because of its maturity, JavaScript already includes ways of handling types in documentation — JSDoc being the first one that comes to mind. If you already use JSDoc, adding TypeScript might seem excessive.
TypeScript is often useful, even for the smallest applications or applications in a Microservices architecture. However, if you have smaller applications it means that there is a higher chance that you can easily infer what the type is. Also, you lose out when you do additional configuration for TypeScript and compile every time to do a few type checks. If your codebase is small, type checking can be more easily done in unit tests, using libraries such as Chai, without the need for TypeScript. Which brings me to another point — you can already check for types without using TypeScript by using test libraries, but TypeScript offers feedback sooner. Before you run tests, and it can sometimes offer feedback automatically whilst you are coding (Visual Studio Code). If you have been given a new application to take care of, have a look at what the existing tests do.
Applications built with more TypeScript
I’m not going to go through all of the advantages or disadvantages of using TypeScript here because there are so many. Please bear that in mind when I start to discuss using TypeScript in this section.
One of the first advantages of using TypeScript that I want to describe is its tooling. If you already use tools like Visual Studio Code (Version 1.43.2) or Visual Studio 2019, there is a lot of support for the language right now specifically in these tools. I will refer to Visual Studio Code as VS Code in the rest of this article.
One feature that is not often available in other tools is, hover information in VS Code. This allows you to quickly see information about types and other information for methods, parameters, variables, or other features of JavaScript, as you hover your mouse over code. You can also hold the alt key when you hover your mouse over the function name of a function call, to see the function that you are calling:
This is helpful when the function you are calling is outside of your view or many lines away.
Signature help (a tooltip shown when you type in (
after a function name to call that function) is another helpful feature that you sometimes don’t find in other good tools that are available right now. The example below shows this happening when you type in errorMessage(
to call the errorMessage
function:
The ability to quickly and automatically remove unused imports is another particularly useful feature that you don’t find in other tools. When you hover over grayed-out imports, a blue indicator that allows you to do this is shown:
A disadvantage of using TypeScript that I haven’t discussed yet, is the change to your project’s file structure and build tools. You would have to rename all your JavaScript files to use the .ts
file extension. If you use JSX, you need to rename extensions to .tsx
. The amount of work that you need to do to migrate would then depend on how many files your project contains, but you could always gradually migrate your files over time.
Also, dealing with declaration and source map files can be a hindrance. For example, when you install declaration files for React and react-dom, you need to add more packages to your project, npm install --save-dev @types/react @types/react-dom
. Integration with frameworks is also a hassle and you would also need to adjust your build tools (Babel and webpack are some examples of these) to support TypeScript. However, depending on which frameworks and build tools you use, good official resources and documentation probably already exist to support this.
One argument against using TypeScript for server-side code is that you could just switch to other type-safe languages on the backend. However, that would depend on what skill set other contributors have. For instance, if your other team members all already know how to write in another type-safe language, then this option is a good use of everyone’s skills. Try to pick the best language based on the task that is required, if you want to deal with data, concurrency, or even types, TypeScript might not be the best choice for each of those topics. The advantage of using TypeScript over other languages is that, well… it is basically just JavaScript… but with types!
Sequentially added properties and strictness of checks
Another issue that might cause problems when migrating away from JavaScript is what the official TypeScript documentation refers to as ‘sequentially added properties’. This is when you create an empty object: let person = {}
and then assign new keys and values to it person.name = ‘John’
. This is an often used and documented pattern in JavaScript. However, TypeScript, by default, does not allow this. The fastest way of getting around this would be to just give person
the any
type, but this comes at the cost of a weaker type check. You can also move properties into the object literal or use a TypeScript interface
.
The last feature of TypeScript that I will discuss is the ability to control the strictness of checks on your code. The strictNullChecks
option is great at preventing bugs because the null
reserved keyword and the undefined
value are common sources of errors in JavaScript. An example I can give is let a:number = null
. In this case, the value of a
can be null
, undefined
or NaN
(you can get NaN
when you set a
to 0/0
). With the strictNullChecks
option set to false
, you might wonder which of the three values a
is set to if all three are allowed. One extra thing to note about this option is that I found that strictNullChecks
seems to already be set to true by default when you don’t use the strictNullChecks
option in the configuration file.
Another option I tested, noImplicitAny
did not work in the way that was expected. We would expect it to raise errors when there is an implied any
type, but this was not the case. You can try this in Visual Studio Code and see that let b: any
is shown when you hover your mouse over b
, let b; console.log(b);
:
However, in this example:
function example (c) {
return c;
}
console.log(example(5));
The noImplicitAny
option works and throws the error: Parameter ‘c’ implicitly has an ‘any’ type. TS7006
:
It is still an excellent option and works as intended, but it does not cover all the cases we would expect it to.
Conclusion
The cost or overhead of adding types to your code is far outweighed by its positives in the long term or for large projects. Remember to always use TypeScript correctly. Learn more about its features and about your own project first, before you consider using it. This will help you to decide if you should migrate to TypeScript, how many of its features you want in your projects, and if your project already has similar features.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post Discussing the over-engineering trap in TypeScript appeared first on LogRocket Blog.
Top comments (0)