DEV Community

Ivan Alejandro
Ivan Alejandro

Posted on • Edited on • Originally published at ivanalejandro0.com

Safer types on TypeScript

Nominal typing on TypeScript

You can use "nominal typing" for a clearer and safer code, as well as to potentially reduce the need of data validation on multiple places.

TypeScript has type compatibility based on "structural typing" but we can get some kind of "nominal typing" with not much effort.

You can read more about TypeScript's type compatibility on their handbook.

Please read the references section on the bottom for information about "structural typing", "nominal typing" and the different approaches that can be used.

Example

Consider this person renaming function:

function renamePerson(id: string, newName: string): boolean {
Enter fullscreen mode Exit fullscreen mode

The id and newName arguments can be any string, they don't have to be valid id/name respectively (for whatever definition of valid we use on our app).

Because of that, the safest thing to do is to actually validate them as soon as we enter the function.

We would also have validation for the person name somewhere on the UI so we can give feedback to the user if there's a wrong name being used.

In order to make sure that the renamePerson function actually receives valid data we can do something like this:

function renamePerson(id: PersonId, newName: PersonName): boolean {
Enter fullscreen mode Exit fullscreen mode

Note that id and newName now have their own type.

How should we define those types?

We can't use something like type PersonName = string because, due to TypeScript type compatibility being based on structural typing, you could send strings for those parameters and that'll just work without complaints from the compiler.

There are several ways of doing this, I'll just pick the simplest one to explain the concept, once you're familiar with the idea you can choose from other variations.

We create a custom type with something specific to it to simulate "nominal typing". Here's an example:

// intersect string with something specific to prevent a structural typing match
type PersonName = string & { __brand: "valid person name" };
Enter fullscreen mode Exit fullscreen mode

Let's use it:

function sayHello(name: PersonName) { /* ... */ }
sayHello("Alice");

// Argument of type 'string' is not assignable to parameter of type 'PersonName'.
//   Type 'string' is not assignable to type '{ __brand: "valid person name"; }'.(2345)
Enter fullscreen mode Exit fullscreen mode

Now, our PersonName typed data is not "structurally compatible" with a string. We have to use our own type.

// any variable of type PersonName must be created here
// that way we guarantee that validation happens
function getPersonName(personName: string): PersonName | undefined {
  if (isValidPersonName(personName)) return;
  return personName as PersonName;
}

function sayHello(name: PersonName) { /* ... */ }
const name = getPersonName("Alice");
if (name !== undefined) sayHello(name);
Enter fullscreen mode Exit fullscreen mode

If we only create a PersonName using this "constructor", and the validation is done properly, we can be certain that everywhere we see a PersonName is going to be valid.

We could validate a string as soon as we receive it from a user or from an API, and then create a PersonName, from that point on we can be certain that we are dealing with valid data.

Playground

Here's a TypeScript playground with some example code: typescript playground

References

Here are some articles where You can read more about nominal typing:

There's also TypeScript's handbook:

An example of this on TypeScript's playground:

Some relevant Stack Overflow answers:

P.S. about people names

I used people names for my example because it's easy to grasp for an example, but remember that defining what a valid name is is not straigforward. See the "Falsehoods Programmers Believe About Names" article:

Top comments (0)