The Power of TypeScript Types
Any frontend developer knows the frustration of seeing the infamous “is not a function” message pop up. It reminds you that, even though your code might achieve your goal, you will not be able to assure its consistency or reliability. Without strict typing there is no stopping tiny slip-ups due to a lack of coffee – or sleep, depending on your dedication.
Aside from your own humanity, there is the clumsiness of other developers. Third party libraries increased the fun by introducing breaking changes. Where and how does that variable get passed around? What accidentally goes right and what mysteriously goes wrong? I repeatedly cast my code into the fiery depths of hell and attempted a rewrite, all to experience the same frustration again.
Moreover, we can track down inconsistencies easily as the compiler will scream bloody murder when it stumbles upon one. As for using those delightful third party libraries, TypeScript offers developers a way to communicate and inform about their code without drowning other developers in a sea of documentation.
I believe this is the core use of TypeScript. However, I also believe we have only scratched the surface of something far more powerful. Let me walk you through some interesting cases.
To know or not to know, unknown vs. any
Imagine we are writing a function that has to return the input as is. We can use the
any type to accomplish this. The
any type accepts everything; strings and numbers; arrays and objects; credit cards and apologies. Well, maybe not the latter, but it will accept any type of variable.
All of this is accepted, because our variable has the
However, there is something fishy going on in the last line. We can shamelessly assign a
number to a variable that was originally declared as a
string type. Not exactly what I am trying to prove here, now is it? Not to worry, here comes the
unknown type to the rescue!
And we are back in business! We can assign anything to the
unknown type, but we cannot assign it to “stronger” typed variables.
Cured with generics
We have just seen how we can use the
unknown types to create a function that accepts anything. However, we have to trust that we did not unknowingly switch the input with some other type of value. After all, our function will return an
To resolve this insecurity we can introduce a generic type. A generic type enables us to assure we return the type that was passed on to our function.
When we call the function we can specify the type of our input by providing what type
T is. The function specifies that we return the same type as
T, thus we know what we get back. Relax and enjoy a Scooby Snack – maybe after reading the article.
As for the function notation, notice I don’t use the arrow notation there. If we want to use the arrow notation can become slightly confusing when using JSX.
You can see that the notation we expect to work, will not work when using JSX. The reason for this is that the compiler confuses the input type definition for JSX syntax. This can be circumvented by adding a comma and omitting the second type. This notation is invalid for JSX and makes the compiler interpret our code as a generic type definition. Phew!
Argument about arguments, with the never type
Take a walk down memory lane. Ever had to write a function that should accept two types of values, but should only receive one at a time? Do you remember writing something along the lines of the following?
“If both values are provided, throw an error because we only should provide one value or the other.”
And perhaps more commonly.
“If this argument is provided, go with this. If it is not provided, go for that”
And god forbid that you forget the most important rule.
“If no values are provided, throw an error.”
It is a verbose – and ugly as sin – practice. Luckily, in TypeScript there is a more natural and less intrusive way of meeting this requirement. Let
never give it a try!
We want the function to accept an object with properties of either types, but not both. We declare the argument we do not want as a
never type, so the compiler will raise an error when we try to pass none or all of the properties.
What is the difference between declaring a property as a
never type or simply removing it from the type?
If we would simply leave out the property we do not want, the compiler will accept calling the function with both arguments. This would again pass the responsibility of handling this case to the function. Do what micro management does best; pass on the responsibility!
Union types, overlapping and Exclude
If we want to allow multiple values we can use
| to create a union type.
Easy enough, so we are moving on to something more interesting.
Instead of combining types, we can use
& to only allow overlapping types. This can be a useful practice when writing a function that will call two other functions that accept some, but not all, of the same types. We can assure only types that will be accepted by both functions can be provided.
Great, we successfully selected all and the overlapping types! However, there is one case left where the unique types are selected. The
Exclude type can be used for this as it will exclude any values that are in the first argument that are also in the second argument.
We are halfway there. We have selected the first non-overlapping type, but the second non-overlapping type is rejected. Let us try and fix that by combining the powers of
And now we are done, “CLAP CLAP” to us!
And now with objects, here are Pick and Omit
Combining object types is pretty straightforward. In the previous section we used
& to select the overlapping types. With objects however,
& results in a type that requires the properties of both types.
It is a little more complex to subtract types. We can use the
Omit type to remove required properties from an object. However, the second parameter of
Omit does not accept an object, but – just like the
Exlude type – a string literal type. We can get the keys of an object using
keyof, resulting in the following solution.
If we only want to keep the overlapping properties we can use
keyof again, but this time with the
Keep track, with Record
If you are like me and you check your pockets every 5 minutes to see if you have lost something, TypeScript is a big relief. With TypeScript you can make sure that you did not forget to declare a key with the
Let us declare two of the “big four” metal bands with some additional information. (Sorry, Metallica and Anthrax.)
Our constant will require both bands to be declared to pass.
Loosen up, with Partial
Now spin that
Record right round. What if we do not need to have all keys? Using our previously defined types, this time together with
We can now define a subset of the bands.
Are we not shooting ourselves in the foot by being so loosey-goosey?
Nope! We now explicitly allow some leniency instead of implicitly allowing everything. And rest assured, we still have to define all properties of the other types!
Put your foot down, with Required
On the other hand, we can use the
Required type to make optional properties required.
Protect your property, with Readonly
We can declare variables as constants to assure their value is not altered. However, when we declare an object as constant, we can still change the values of its properties. We can use the
Readonly type to prevent this.
We simply wrap our type in the
Readonly type and that about wraps it up!
Of course, there are many other use cases and many more interesting features. But, with these examples we have seen TypeScript forces us to be strict, while also providing us plenty of flexibility. Whatever types you are working with, there is always a way to get what you want. Meanwhile it makes our code reliable and prepares any project for growth. With the established support for TypeScript and the many projects that use it, we can say that TypeScript does not go anywhere soon.
Enjoy and explore the power of Typescript types!