How to write as little type declarations as possible and still get the best results

TypeScript has gained a lot of popularity in the web community after being introduced in 2012, which has only increased after Angular, Google’s powerful enterprise framework, decided to use it as its language for the web. TypeScript is particularly popular among developers that come from another (usually backend) languages with static typing like C# or Java.

In this article I am going to explain several best practices about type declarations in TypeScript. Because TypeScript is very similar to JavaScript, I am not going to assume any prior knowledge of the language, but rather introduce it in a small paragraph. If you are already familiar with the language, you can skip to the next section.

But what is TypeScript, anyway?

TypeScript was introduced as, and still considers itself, a superset of JavaScript. That means that any code that is valid in JavaScript should be valid in TypeScript (if no type declarations are introduced!). Actually, TS is mostly just JS + type declarations + some small features (private/public/protected fields on classes, tuples and enums).

Here is a small piece of code in JS:

Not type declarations, no safety

Of course this code is perfectly valid, the only problem being that another developer using this function is not required (well, strictly speaking) to pass a number as the age argument. Of course, in this case the name of the argument speaks for itself, but what if it is not that obvious?

What values are acceptable as status?

In this case we have a status field, which the business dictates can only be Pending , Completed , or Cancelled . But how can we enforce that? There is nothing telling the other developer that they cannot pass, say I don't care as a value for the status argument. Of course, we can always do this:

But this way we just added 4 lines of code to type-check. And it will perform whenever the createOrder function is invoked during runtime! Also, the editor in which the developer works will still have no clue on whether the code is correct or not. So here is how TypeScript helps us:

This looks way better

Here we just declared types for each of our arguments, explicitly defining what is and us not allowed to be passed to this function. This liberates us from a lot of confusion and frustration in the future, and also makes our code more instantly understandable.

If you want to know more about TypeScript, feel free to head over to the official documentation and explore it.

Too much types

But of course, with every tool comes the ability to misuse and even abuse it. Consider this piece of code:

This code is not necessarily bad, but it can be improved a lot. As it appears, in TypeScript you do not have to declare types every time you declare a variable or class property. Most of the time TypeScript can understand the types by itself, using a technique called type inference.

Here is what our code will look like after we change it to allow for type inference to work:

As you see, this is exactly the same code, just some type declarations are missing now: thing is, when we initialize something to data, we already tell TypeScript the types!

Of course, this is going to make our lives easier, and also, help us make the wrong assumptions!

Not enough types

If we know about type inference, we may do something like this:

This looks normal to the eye, but in reality… We assumed that createProduct function returns an instance of class Order<ProductType> , of course, TypeScript inferred it so, and ProductType is inferred to be a string , well, because we invoked the createProduct function with a string. It would be entirely correct, of course, right now, but really isn’t. Consider this: another developer decides that it is time that some additional metadata is provided about every product, so they create a new ProductWrapper class to incorporate that metadata. Here is a naive implementation of this class:

It holds some metadata about the product and also the product itself, and this class is also generic itself. Notice how we do not alter the Order class, it still can accept either a usual type like a string , or a ProductWrapper instance. This is not a problem — it is entirely possible that the business logic requires us to have such a class, the problem is the factory function we created, more precisely, that it does not have an explicitly specified return type! The return type of the createOrder function now is really Order<unknown> ! Here how it comes down:

Now every time we invoke this factory function, we may assume we get type Order<T> , but in reality we have Order<ProductWrapper<T>> , and TypeScript even does not know about that. Solution to this is simple:

Always declare types for arguments and return value of a function

This code is far more obvious and safe:

Now, anyone modifying this functions will now, that as it has an explicitly defined return type, will know not to change that right away, as other variables depend on the declared return type.

Tuples

Tuples are (in a way) a specific type of Array, that always is expected to have the same length, and has the same types on the same positions of it. An example of a tuple is a mathematical vector:

This is a 3D Vector, but actually, we still can .push another number into it, making it a 4D Vector. This is not what is usually expected of a Vector, but it is a normal behavior for a TypeScript tuple, even after we explicitly declare it as a tuple:

Completely valid code

Coming from other languages, especially from Python, one may confuse the tuples from there with TS tuples, which are basically just Arrays with a fancy type declaration. Here is a rule of thumb to avoid this:

Do not use tuples as substitutes for things that may be expressed as a class

It would be better for us to implement a simple Vector class and use it instead of a tuple, that way we can explicitly enforce all requirements and also have additional functionality.

Too generic

TypeScript generics are awesome, aren’t they? But here are some pitfalls. When using generics, we imply that a class can be a generic type of any type out there. It is not always the case.

Let’s say we are implementing a small class for state management. Obviously, it is going to have a generic field in which the application state is stored, and it is going to be stored in an object (if it has just one field, why bother with having a full blown stat management system?), and that object ha a very specific interface (state can change during runtime, but not it’s shape). So this is the very example we need. Here is a naive implementation:

This is not bad in itself, but here comes the problem: what if someone wrongfully assumes they can have a store just for one basic item, like a string ?

The second line is going to throw an error, because the rest operator is not working on a string . How can we make sure this does not happen, while still allowing for any shape of the state?

Now we just explicitly declared that our state may be only an Object of any shape! Any attempts to initialize it to anything else will fail.

Rule of thumb:

Be careful with generics, make sure they are narrowed down to specific types if needs be

Combining types

This is my favorite type about TypeScript: instead of having function or method overloading, TS allows us to have combined types — unions and intersections of them. Here are some examples:

A function that can accept a Date object or a string with a Date in ISO format

This is very useful, but can tricky sometimes. Consider this two classes:

As you see, this different classes share one method. It means it is possible to have a function that can make use of both classes:

Here we use the same method that both the objects have, so it is not possible to make a mistake. But what if we want to invoke someOtherMethod if param is of class A and someOtherMethodSpecificToThisClass if it is of type B ? We can use the instanceof operator, of course:

As you see, we changed the type declaration to A & B , to signal that now both types are in a union, and methods from both A and B can be used inside the function. But the compiler suddenly gives us an error:

Type ‘never’? What the hell?

What does this even mean? What it means actually is that because we defined the param argument’s type to be of A & B , the compiler assumes that the first checking of the instanceof operator is going to be true, and the else clause will never be evaluated. Read more about the never type here).

So what we should actually do is to change the type declaration back to A | B , signifying that in one case param may be of type A and in another case of type B .

Conclusion

TypeScript is a very powerful tool, which allows us to largely improve both our code readability and safety. But as any tool, it comes with several culprits, which I hope to prepare you for with this article.

Follow me on Medium and Twitter for more on Angular, Rx.js, React, and JavaScript in general.