Correctly working with types in Typescript might be very confusing (if not maddening and mind-blowing) for someone who comes from the JavaScript world. Our company has started migrating from JavaScript to TypeScript not so long ago, so most of the developers are still learning how to work with it (including myself). For some people this article might be trivial, but it was a real challenge for me. And I believe that sharing experience and knowledge about challenging things is the best we can do because it could help someone with the same challenge.
Problem
So, once, when doing a code review of a merge request from another team, my colleague and I stumbled upon these two peculiar functions:
Let’s skip the review of their actual code, it’s not really relevant for this article, they are essentially transformers of objects to Maps of values of a certain object by this object keys. It’s not the insides of the functions that caught our attention, but the way those functions use the TS types. I, along with my colleague, immediately saw the flaws of these functions’ typing thanks to our previous experience in Kotlin and C++ respectively.
The functions were supposed to be used something like this:
It’s the real function from the same merge request, but the names and the object structure have been changed — sorry, NDA makes me do that ☹️
Anyway here we see two main problems:
We have no arguments safety — we can pass ‘blah’ and ‘blahblah’ as parameters to our mapByKey function and Typescript won’t stop us from it.
We have to do the Type assertions to “calm” Typescript down, adding a substantial risk of an error. This is because TS will treat the result variable as a Map of any T[keyof T] to any Tkeyof T, and any type assertion that satisfies those types will be “legal”.
If you are not sure what this all means, I recommend you to read some Typescript documentation on keyof operator and generics.
Partial Solution
The first solution that comes to our mind is to use not the string type in our mapByKey function, but keyof T:
It will definitely help Typescript stop us from making, for example, a spelling mistake:
Looks better already, but what do we do about the second problem? How do we get rid of this type assertion and make the code safer? How do we “tell” Typescript that the resulting Map in those function contains not some abstract keyof T elements, but the types that we know with 100% certainty (because we know what “keys” of an object we passed to the function, and we know their types from the object structure).
The Real Solution
After some digging and brainstorming (and long moments of despair) we finally found the solution — we need to add more generic parameters and use type parameters with constraints. This way Typescript will be able to know the exact types of the function arguments. Our functions will now look like this:
And now it becomes so much easier and safer to use them:
Now Typescript is on guard and is even helping us to write the code!
Conclusion
Generics and other concepts might be scary, but try to make Typescript your friend, and not your enemy. Explore the Typescript documentation and other resources extensively, don’t be afraid to try out stuff, and don’t get demotivated when something doesn’t work. In the end it will absolutely work out. And when it does — it will do wonders for you.
This is my first article, and I'm planning to write more, so any feedback is very much appreciated!
Photo by Mohammad Rahmani on Unsplash
Top comments (0)