Using precise types with Typescript

Typescript has a lot to offer when it comes down to… types. Which makes perfect sense, right? Let’s take a few examples based on the following interface:

The following variable is meant to store the id of a User in our application. The type of that id is a number, so this declaration is accurate:

But could be a lot more precise if we used the following syntax instead:

Now we are expressing that id is of the same type as User id (which is still a number), and we make it explicit that we’re using this variable to store a User id, not just any number. We are using an indexed type.

We can use a similar approach for our language. Instead of being any string, we can narrow that type using union types:

A user’s language can only be one of the four listed values. This makes our code safer (a typo would be caught by our compiler and IDE instantly) and doesn’t impact performance (union types do not get compiled into anything).

Partial utility type in Typescript

Typescript is excellent for type safety as it prevents us from making mistakes when declaring new objects. That said, an object is sometimes created by aggregating data from different sources (different API calls, for instance).

For instance, say we have the following interfaces:

We have to create an instance of the Question object, but let’s imagine that creating that object requires several steps. In the first step, I retrieve data for the question itself, and in the second step, I have to retrieve the list of options.

At first, my Question object would look like this:

But this code won’t compile, as myQuestion is missing the options property. We could assign an empty array to options, or we could make it optional in the interface using this syntax: options?: Option[];

All of that seems like cheating, though. Instead, we can use the Partial utility type to indicate that our object is in a temporary state:

The compiler is happy with the above code; we don’t have to change our interfaces.

myQuestion is now an object that implements a subset of the Question interface, with all properties marked as optional.

Typescript has several more utility types like Partial. I’ll cover more of those in the upcoming newsletters.

Path mapping to simplify verbose imports

When an Angular application contains several modules or sub-folders, it can become challenging to maintain the readability of your Typescript imports. For instance:

import {something} from '../../../../../shared/shared.service'

It’s always better to rely on your IDE to write those paths for you using autocompletion (usually ctrl + space). Still, imports don’t have to be that long. Typescript has a solution for that called path mapping.

The idea is to define common paths in the tsconfig.json file of your projects. Then, in the compilerOptions property, add or edit the paths config to provide some aliases for all of your long paths:

"paths": {
  "stubs/*": [
    "./src/app/stubs/*"
  ],
  "state/*": [
    "./src/app/state/*"
  ],
  "shared/*": [
    "./src/app/shared/*"
  ]
}

The above results in a shorter import syntax everywhere in your code:

import {something} from 'shared/shared.service'

Of course, you can define as many path mappings as you need. Read that post for more of my favorite Typescript tips: 5 Typescript tricks for Angular. Some have already been covered in this newsletter, but all need reminders occasionally.

How to generate DTOs for data objects?

Yesterday, we saw how to use json2ts to generate type definitions for our Angular applications. In addition, we saw that interfaces are optimal for performance reasons. Yet, the caveat with interfaces is that they can’t act as a Data Transfer Object (DTO) on their own to change the data you’re dealing with, nor do they perform any runtime checks.

As a result, some people might prefer to use Quicktype over json2ts as it has more configuration options when generating code from a JSON string:

As you can see, those options enable additional capabilities, such as using union types instead of enums (as recommended earlier), throwing errors if an object you’re parsing doesn’t have the right shape, and even some DTO operations such as using camel case instead of underscore in property names.

Of course, you can tweak that generated code and add additional DTO rules as needed.

Quicktype also supports languages other than TypeScript, so you could use that same tool to generate the server-side types for your data in Java, C#, or Python, for instance.

How to generate type definitions for TypeScript?

This week, we have been looking at union types and the downsides of using any or unknown when we don’t have any accurate type information.

Today, let’s look at how to generate type information out of any JSON data. The first tool I want to mention is json2ts.com, a website where you can copy-paste any chunk of JSON syntax and get a fully-typed output of interfaces with inferred types and everything.

For instance, if I copy-paste the following JSON into json2ts:

We get the following set of interfaces ready to be used in our code:

The more data you give to json2ts, the more precise it is. For instance, if you give it an array of similar objects, json2ts can identify if some properties are optional or support multiple different types.

Also, it’s worth mentioning that interfaces are better than classes to describe type information because they do not get compiled into anything at all (like union types), thus bringing type safety to your code without making your production code bigger.

Union types in TypeScript

Yesterday, we talked about any, unknown, and the pros and cons of using such types.

Today, let’s focus on union types, an alternative to enums that are more performant and flexible to describe custom types. Let’s assume we need to support different currencies in our application. Since currencies are mostly a code/symbol, we could take the easy route and do the following:

But this doesn’t help us much. For example, is any string a valid currency? Definitely not. So a better option would be to create an enum for it and describe all possible values in it:

Now we have a proper type to describe what a Currency is. But enums aren’t that convenient because they can’t be used as-is in an Angular component template, requiring some workarounds. Also, they get compiled into a monster structure of JavaScript that increases the size of your code bundle for no valid reason since we don’t have type-checking at runtime in the browser.

Enter union types

Union types solve all these problems. They’re lightweight and do not get compiled into anything at all, which means they’re always more performant than enums (less code to download + less code to interpret = faster app in the browser). They guarantee type safety and can be made out of anything, including strings:

A lot more can be done with union types, such as having more options than just one single type for a given variable:

Angular uses union types a lot. For instance, if you’ve ever used the canActivate router guard, its signature supports up to 6 different return types, all defined with union types:

And we can have unions of union types when more granularity is needed with specific sub-types:

TypeScript: any vs. unknown

TypeScript has some abstract types that can be helpful in Angular applications. Most people have encountered the type any at some point, and it has become a typical anti-pattern in several projects where developers decided: “I don’t want to bother using proper types for this object, so I’ll use any.

Here is why any is dangerous and not recommended:

Now, if we replace any with unknown, things look different:

As you can see, unknown preserves type safety. If we receive an object from a third-party library and need to pass it around to another function, unknown is perfect for that.

One way to think about unknown is: We have this object that we don’t know what’s inside, so we won’t allow touching it; we’ll store it or pass it around.

I can’t think of good reasons why we would need to use any in Angular code at this point. Using any is refusing to use TypeScript properly and falling back to untyped JavaScript.

In the next few days, we’ll cover different techniques and tools we can use to create accurate type information so we don’t need any or unknown anymore.