If You’re Always Using Any For Ambiguous Situations You Are Doing It Wrong. There Are Better Safer Alternatives.
When you first start using TypeScript any situation where it is difficult to know the right type for a variable may cause you to set the type as any. Obviously this is not ideal as the whole point of using TypeScript is to have clear types set so that the compiler can help you catch errors — before your code makes it into production. Let’s see what methods there are already in the language that allow us to avoid using any and therefore gain the full benefits of using TypeScript.
This is a type that I’m sure most TypeScript developers are aware of, but only a few are actually using. In addition there are some features of unknown that make it significantly different and safer to use than any. For example you might know that the unknown type allows setting of a variable to a value of any type. And that value can also be reset afterwards to a completely different type, just like the any type. However this is where the similarities end.
In the case of type any the compiler will not do type checking on your behalf. Once you set something to type any the compiler simply gives up and leaves you to your own devices. But in the case of the type unknown the compiler believes that your variable actually does have a type. However it does not know or even try and guess what that type is. It demands that you verify the type yourself and prove that it is of one type or another. Only then will the compiler allow you to either call into the variable’s members or set the variable to another variable of the same type (note without these checks it is still possible to set an unknown to another unknown or to an any). Let’s take a look at some examples of how unknown works.
First let’s look at a sample any variable.
Clearly this sort of runtime failure could be avoided by using a proper type declaration. Unfortunately not every situation provides clear information on what the correct type of some variable or return should be. For example when calling a web service or accepting json data. The type information may not be clearly defined or not directly compatible. It may even change under certain circumstances. This is why the unknown type is so useful. Let’s look at the same example using unknown as the type.
As soon as you enter this code you should see your editor complain about the function myNonExistantFunction, that it does not exist for type unknown. But here’s the thing with unknown it will not allow you to call any members of a variable or set that variable to anything until you have proven that it is of the type required. So if I do this it also complains.
This may be unexpected, because clearly right above the line val.push() I set the val variable to a new array. So then how do I make use of a variable of type unknown? Let’s update the code so that adding to the array works.
So there’s two things to note here. First an unknown variable or return does have an actual type above and beyond unknown. So a simple way of thinking about it is that the type unknown is more like a label and “hidden” inside is the real type. Second once I’ve confirmed what type is actually being used only then can I call into the variable members or set it to some other variable of the same type.
So unknown is a good alternative to use instead of any in situations where it’s not immediately clear what type something should be or if that type could potentially change. However there is another way to deal with type ambiguity, union types.
In the case of union types they can be used similarly to unknown types. However they are a little less flexible in that you must know up front all the possible types that may be returned by some call. Here’s an example to clarify.
In the above example the result of compiling and running this code is this.
If you’re a TypeScript developer you’re already familiar with this type. However you may not have yet used it for difficult typing situations. For example for a recent project on my app, DzHaven, I had to setup payments. And the payment provider returned an enormous object for one of its calls. It had several dozen properties and some of those properties in turn had their own properties. However I was only using a tiny subset of those members. Additionally the payment vendor was not using TypeScript so it was not always clear what type each field should be. It would have taken hours to figure this out, but the return on time investment would have been very small since as mentioned I only needed a few fields.
These kinds of situations can cause some developers to just set the object as type any and be done with it, but again there’s lots of issues with doing that. So what I did instead is defined my own interface with only a subset of the fields and just set the return type as that. Here’s a possible example of such a scenario to clarify what I mean.
If you compile and run this code you will see that it compiles and runs without error and returns this.
You may be wondering why it returns all the fields instead of just the ones that are part of the type declaration. This is because the compiler does not change the set data, it only checks for a type match (the fields of type MyData). So when console.log runs it returns the entire object’s members.
As TypeScript continues to grow in popularity type definition files will continue to fill in type information and reduce the amount of ambiguous typing scenarios we face. However until that happens completely, as TypeScript developers we need to be careful to use the language in a best practices sort of way.
Cool that was a small overview of the ways in which you can maintain type safety in your TypeScript code and use the language in the way it was intended. Happy coding.
As always if you like helping other devs try DzHaven.com