Type safety and generics do not have to be complicated!

Ever find yourself consuming a third party library, or API, with Typescript? Ever been in a situation where whatever you’re working with, isn’t exactly strongly typed, entirely neglecting one of the advantages of Typescript?

Me too!

But fret not, there are things we can do to ensure type safety even when working with libraries, objects or return values that aren’t typed. We can achieve this using a bit of generics gymnastics; stretching ourselves to apply advances generics to achieve what we need, in a maintainable way.

Disclaimer

Before we dive in, I want to emphasize that it is my strong belief that often when we find ourselves needing to reach for complex generics or type definitions, it is usually a red flag / an indication that something is at fault with the overall architecture of what we’re doing.
As a functional language most things – even with types – should be relatively straightforward and only very rarely require complex type definitions.

Let’s define the problem

For the sake of the example, let’s imagine we have an external library that provides us a method external() which we know from the documentation takes multiple arguments.

It’s easy to get caught up in thinking we have to do a lot of clever types, combining union types with Omit<>, Partial<> and more just to satisfy the exact requirements of some external libraries.

I’m here to tell you otherwise!

Firstly, I will argue the case that 100% Type safety isn’t necessary, and in fact isn’t even ideal.

Secondly, perhaps more of an extension to my first point, it is simply OK to use any or unknown every once in a while, especially when working with third party tools, because often we can rely on their error handling to provide us what we need.

I often find that trying to satisfy a codebase with 100% type safety, is a Typescript pursuit which means we, at least to some extend, forget that Typescript is simply a superset of Javascript. It’s totally fine to rely on Javascript, particularly in cases when it’s well thought out.

Let’s say you’re stretched to your ability trying to come up with a clever generic in order to satisfy TS constraints for the method of some external library. We have to remember as developers, we’re only ever as productive as the value we can provide to the business, so while coming up with clever typings can be fun, I often believe it is not worth the time invested.

In a perfect world, we have 100% type safety everywhere and we can’t import an external library which doesn’t have their types defined, which can’t have dependencies that don’t have…. And so on and so forth.

But we don’t live in a perfect world.

So instead, use that any or unknown every once a while, check out the docs and if the error handling seems to be great, what have you got to worry about? That unknown will get turned into an error, so all your type checks are completely arbitrary anyway, because mostly we only do types for optimistic cases.

So what’s a better aproach?

If you find yourself in the situation described above, and if you find my stance a bit snarky or you simply do not agree with me – that’s totally fine by the way – there are however two things I recommend you try:

  1. Take a break!
  2. Zoom out

Take a break

Yes you heard that one right. In my experience, if we’ve gone down far enough into a rabbit hole where we feel like we need or must create this creative new complex type, often it can be a good idea with a break.

Especially one that involves getting some fresh air.

This is a time-worn, proven and battle-tested strategy so I won’t say too much about it. You know the deal!

Zooming out

Zooming out on the other hand is when I find we’re suddenly making complex types for an application architecture that may be faulty in the first place.

With faulty I simply mean, perhaps we’re overcomplicating things. As much as we may argue the opposite, developers are also human and humans suffer from complexity bias.

Therefore, getting some perspective on the task at hand, seeing if we can somehow simplify the problem or the application architecture often ends up resulting in simplifying types.

Refactoring is your friend.

Often what we’re typing are data structures. So an example of the above would be that, if we find ourself with an overly complex data structure – which needs complicated typing – it may well be a sign we are just not using an optimal data structure. It could be that what we’re trying to do is too generic. Yes there is such a thing!

Without pointing any fingers, this is where I often see newer developers slip up. The belief/impression that a codebase must be 100% type safe combined with trying to do ‘too much at once’ (ie. a complicated (too generic) data structure!), is often a combination. that tends to lead to poor application architecture.

So there you have it

In the end my stance is that, things can get too generic. Human complexity bias combined with trying to be clever with types, often results in doing things in a suboptimal way that’s costing precious time.

If you find yourself in such a situation try to take a break or zoom out, and see if you can detect any red flags after coming back to it.

Remember: Typescript is here to help us make things easier, not make them more complex 🙂

Leave a Reply