6

Better typings for strings and numbers in TypeString using opaque types

 2 years ago
source link: https://kulak.medium.com/better-types-for-strings-and-numbers-in-typestring-using-opaque-types-c9153eb8009c
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Better typings for strings and numbers in TypeString using opaque types

Opaque types in TypeScript

TypeScript is a great tool to ensure your code is correct and you are not passing incorrect data around. Unfortunately, there are still cases when we can’t guarantee that the types are correct — especially when dealing with primitives like strings or numbers.

In almost every modern application we use strings to represent a bunch of different types of data: tokens, resource identifiers, names of events, generic data, template strings, and much more. Each of those use cases is very specific, yet, all of them will usually end up being typed as a generic string.

Examples of different uses of strings, auth token, api server url, etc. They all get typed the same as string in the end.
each of these strings has completely different use-case yet we type them the same

Sometimes it is obvious that one is not interchangeable with another but in other cases it might be tricky without relying on manual testing.

When it comes to numbers, they can represent: currency, virtual points, internal counters, timestamps, etc. Again, in our apps they all are typed as a number.

The infamous example of incorrect use of types that ended up catastrophicaly is Mars Climate Orbiter: one piece of software incorrectly produced results in United States customary units (pound-force seconds) instead of SI units (newton-seconds). It resulted in probe deorbiting on 23 September 1999 and failing $327.6 million mission.

In this article I will deal with slightly less severe examples. I will try to show the drawbacks of using primitive types directly and introduce you to solution: Opaque Types (also commonly referred in TypeScript as Brands).

Opaque Type

Opaque type is a type which implementation is hidden to the application as a whole, and is accessible only in a specific function or a file. Thanks to that we can ensure the data was created only in a specific place.

Unfortunately due to the duck typing, JavaScript does not really have such mechanism in place. Almost every object can be mimicked by replicating set of its properties. Moreover, nor JavaScript nor TypeScript provide any way of creating distinguishable type aliases (aka. non-structural or nominal typing).

The solution to overcome those limitations is fortunately straight-forward:

The code above will allow us to distinguish between IPs and URLs even though the underlying type in both cases is the same.

Example with two tokens

Let’s imagine the following scenario. We have a system that uses authentication server to obtain token, which later allows up to communicate with the API. To obtain it, we need to send another token to the auth server. This logic is represented in the diagram below:

Let’s implement this logic in our code using regular strings:

Can you spot the bug in the code above?

The bug: Instead of using apiToken as an argument togetData method we use the token used to authenticate us with the auth server. In the best case, we would find this error while testing, in the worst, it would end up on production — both scenarios require runtime checks so we cannot use any advantages of the typing system.

Fortunately, there is a solution. We can mark the strings with different “types”, making our code resilient to that kind of errors:

The most interesting part of the code above is anOpaqueString type — it’s “marking” our string with an additional __type field — this field is never meant to be accessed directly (that’s why it is prepended with double underscore). Instead, TypeScript will use it to determine if two types are equivalent to each other. Thanks to that, the code above will result in the following error:

Argument of type ‘AuthToken’ is not assignable to parameter of type ‘ServiceToken’.

Example with currencies

The same technique can be used to distinguish between different number types. Imagine you are working on an online store and you might have products in different currencies. Summing up the values of products in different currencies does not make any sense and can cause inconsistencies in the system or even a revenue loss. To make sure we’ve implemented all necessary fail-safes in our system, we could use the following opaque type:

The code above will throw a type error when trying to pass the amount in philippine peso (PHP) to computeUKVat method.

You can also use this method to do more advanced checks like the one below:

The code allows you to pass products in a single currency only. The first sumProducts call will not throw any error because all the arguments are in EUR. The second one will fail because its parameters are of different currencies. To detect that we use NoUnion helper type that ensures types do not make a union (they would normally resolve into Currency<”EUR” | “GBP”> which we try to avoid.

More generic solutions

The problem is not new and there are already existing solutions for it. Many TypeScript utility libraries provide helper generics for it:

If you have used Flow before, you might be familiar with its Opaque Type Aliases which provide similar functionality.

Native TypeScript implementation for similar solution has been discussed in TypeScript community since 2014 but it doesn’t seem to be any consensus so far. Fortunately the solution is straight forward so you can start using it in almost every project if you’d like.

Would you try using one of the described solutions in your next project? Or maybe you have used similar technique before? Share your thoughts in the comments!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK