Photo by Yen Vu on Unsplash

Preventing Logical Errors in TypeScript with Nominal Typing

Prevent SQL injections, HTML injections, force function returns, force value checks, and more by using nominal types in TypeScript.

kevinMEH
Level Up Coding
Published in
6 min readMar 18, 2024

--

Structural Typing

As most of us know, TypeScript compares types using the structure of the type instead of using its identity. Take this example below:

In the example above, both Person and Dog have the same internal structure: they both have a string property called “name” and a number property called “birthday”. Therefore, TypeScript considers them to be the same type.

Even though the function printInformation accepts a parameter of type Person, we are free to pass in an object of type Dog and TypeScript will consider it to be valid.

This is in contrary to strictly typed languages like Java or C++: if you have two classes or structs with identical contents and identical methods, you will not be able to use one in place of the other.

Most of the times, this is perfectly acceptable and desirable behavior. If TypeScript only compared the names of types instead of their contents, we will encounter a “Type Mismatch” error every time we try to do something like this:

This is because a generic object of type “object” will no longer be acceptable as an input for the type “Person” even though they may have the same internal structure.

Nominal Typing

However, sometimes we want to avoid this behavior. For example, what happens if we try to pass in a Dog object into the printInformation function?

In that case, the printInformation function will treat the Dog object as a Person, and print “This person is called Fluffy.”, which is obviously incorrect.

To prevent this from happening, we can force type checking based on identity rather than structure by performing a trick using TypeScript’s type intersections feature:

We’ve added a new attribute called __type to both Person and Dog, with the type of the attribute being a unique string literal. Now, TypeScript will treat the Person and Dog type as two separate types, just as if it had used nominal typing. If we then try to pass in an object of type Dog into printInformation, the following error will be raised:

This is because the types of the attribute __type in Person and Dog are now different; in Person, it is of the string literal type "Person" and in Dog, it is of the string literal type "Dog".

The Person and Dog example here might seem a bit silly. How often do situations like these arise where we need to differentiate between two identical types? The answer: more often than you might think. Let’s look at some practical examples below.

Preventing Injection Attacks

Preventing SQL Injections

Let’s say you are building a web application that uses a SQL database on the backend. Your API accepts user inputs, and performs SQL queries based on the inputs. To prevent SQL injections, we employ a sanitize function to neutralize user inputs and ensure nothing weird is going on. Then, we run the query using the sanitized input:

However, there’s a problem here. The executeQuery function takes an input of type string. But what happens if we forget to sanitize the inputs before executing?

According to TypeScript, nothing is off here. After all, an unsanitized input and a sanitized input is of the same type: string. To fix this, we may want to declare a separate type “SanitizedString”:

However, since TypeScript compares types based on structure and not identity, we are still allowed to pass in a string to executeQuery. To prevent this, let’s use nominal typing and the identity trick from above. Now, when we try to use an unsanitized string, TypeScript throws an error:

We’ve just leveraged TypeScript’s typing system to completely prevent SQL injection attacks!

Preventing HTML Injections

We can also use nominal typing to prevent other types of injection attacks, including HTML injection attacks.

In the example above, since the generatePage function only accepts inputs of the type SanitizedHTML, TypeScript will throw an error when we try to pass in an ordinary string into the function.

Forcing Value Checks

On a related note, we can also use nominal typing to force a value to be checked by a function before being used.

Let’s say that we are creating a file sharing service where users and upload and download files to some server. Before we fulfill a users request, it is necessary to check that the user has provided a valid path to make sure that the user is not requesting a file they shouldn’t have access to.

But once again, what happens if we forget to check if the path is valid before calling getFile?

Similar to the case where we always want our inputs to be sanitized before being used, we can define a separate ValidPath type which is only returned by checkValidPath and have the getFile function only accept inputs of the ValidPath type. This way, TypeScript will start complaining if we don’t check a value for validity before passing it to getFile.

As a rule of thumb, any time we need to ensure a value is processed through some function, we can use this feature by declaring a new type as the return type for the function.

Forcing Function Calls

Another way we can utilize nominal types is for forcing function calls. One scenario where this would be necessary is when we’re designing API routes where authentication is required.

Let’s say we are designing an API route for changing a user’s password. Before we perform a password change, we need to make sure that the person requesting the password change owns the account first.

However, it is very easy to forget to authenticate a user’s identity. To make the authentication process more conscious and explicit, let’s make the POST request handler return a new type called “AuthResponse”, representing an authenticated response. The only way to return an AuthResponse is by calling the authAndExecute function, which authenticates the request before executing a given function.

Now, if we try to perform a request without authenticating, TypeScript will throw an error reminding us that we are not returning an AuthResponse:

To fix this error, we are forced to authenticate using the authAndExecute function, which is the only way an AuthResponse can be returned.

Closing

Being a better programmer doesn’t necessarily mean making fewer mistakes, but rather knowing how to prevent oneself from making those mistakes. Nominal typing is one technique which developers can utilize to prevent a multitude of such mistakes, as demonstrated above. It’s a powerful technique which every developer should use to help make their programs safer and easier to extend.

I hope you enjoyed this introduction to nominal typing. As always, if you have any questions or comments, feel free to leave them below. Until next time!

--

--