TypeScript — Tips & Tricks

More and less known ways to improve your TypeScript code

I’ve recently been a guest on the “Angular Academy Show” (watch it here) to demo some cools behaviours of TypeScript. Since people were interested in the source code, and I realised that it will be even more valuable with some comments, I’m putting it here as this article.

Tip 1. TypeScript & DOM

When you start using TypeScript you will notice it’s quite knowledgeable when it comes to working with in a browser environment. Let's say I wanted to find an <input> element on the page:

const textEl = document.querySelector('inpot');
console.log(textEl.value); 
// 🛑 Property 'value' does not exist on type 'Element'.

So TypeScript shows me that error, because… I have a typo in the querySelector method and instead of "input" I'm looking for an "inpot".

How does it knows that? The answer lies in the lib.dom.d.ts file which is a part of TypeScript library and, basically, describes everything (objects, functions, events) that occur in a browser. Part of this definition is the interface that is used within thequerySelector method’s typing and maps specific string literals (like "div", "table" or "input") to corresponding HTML element types:

interface HTMLElementTagNameMap {
"a": HTMLAnchorElement;
"abbr": HTMLElement;
"address": HTMLElement;
"applet": HTMLAppletElement;
"area": HTMLAreaElement;
"article": HTMLElement;
/* ... */
"input": HTMLInputElement;
/* ... */
}

It’s not a perfect solution because it only work for basic element selectors, but still better that than nothing, right?

Another example of this ‘smart’ TypeScript behaviour is when you work with browser events:

textEl.addEventListener('click', e => {
console.log(e.clientX);
});

.clientX in the example above is not a property available on any given event - it's only available on MouseEvent specifically. And TypeScript figured the type of event out based on that "click" literal that's the first argument in addEventListener method.

Tip 2. Expect Generics

So if, instead of an element selector, you use anything else:

document.querySelector('input.action')

then the HTMLELementTagNameMap won't be useful, and TypeScript will just return a fairly basic Element type.

As with querySelector it's often the case that a function can return various different structures and it's impossible for TypeScript to determine which one will that be. In that case you can pretty much expect, that said function is also a generic and you can provide that return type in a convenient generic syntax:

textEl = document.querySelector<HTMLInputElement>('input.action');
console.log(textEl.value);
// 👍 'value' is available because we've instructed TS
// about the type the 'querySelector' function works with.

If you work with Angular then another example could be the ElementRef class:

Tip 3. “Did we really find it?”

That document.querySelector(...) method doesn't actually always return us an object, does it? An element matching that selector might not be on the page - and instead of an object the function will return null. So accessing that, say, .value property might not be all that save... by default.

By default, the type checker considers null and undefined as assignable to any type. You can make this more safe and restrict this behaviour by adding strict null checks in tsconfig.json:

{
"compilerOptions": {
"strictNullChecks": true
}
}

With that setting, TypeScript will now complain if you try to access the property on an object that is possibly null, and you will have to “reassure” it about the object’s existence, e.g. by wrapping that part with if (textEl) {...} condition.

Other than querySelector, the other popular case for this is Array.find method - the result is possibly undefined.

You don't always find what you're looking for :-)

Tip 4. “I’m telling you, TS, it is there!”

As we’ve established, with strict null checks TypeScript will be much more sceptical about our values. On the other hand, sometimes you just know from the external means that the value will be set. In such exceptional cases you can use “post-fix expression operator”:

const textEl = document.querySelector('input');
console.log(textEl!.value); 
// 👍 with "!" we assure TypeScript
// that 'textEl' is not null/undefined

Tip 5. When migrating to TS…

Often when you have a legacy codebase that you want to migrate to TypeScript, one of the bigger hustles is to make id adhere to your TSLint rules. What you can do is edit all these files by adding

// tslint:disable

in the first line of each, so that TSLint don’t actually check them. Then, only when a developer works on a legacy file, he will start by removing this one comment and fix all linting errors only in this file. That way we don’t do a revolution but rather an evolution — and the codebase gradually, but safely, will improve.

As for adding the actual types to that old JavaScript code, you actually can often do without. TypeScript’s type inference will take care of it and only if you had some nasty code, e.g. that assigns different type of values to the same variable, you can have a problem. If refactoring that is not a trivial issue you can (sigh..) resolve to using any:

let mything = 2;
mything = 'hi';
// 🛑 Type '"hi"' is not assignable to type 'number'
mything = 'hi' as any;
// 👍 if you say "any", TypeScript says ¯\_(ツ)_/¯

But really really, really use it as a last resort. We don’t like any in TypeScript.

Tip 6. More type restrictions

Sometimes TypeScript can’t infer the type. Most common case is a function parameter:

function fn(param) {
console.log(param);
}

Internally, it needs to assign some type to param here, so it assigns any. Since we want to limit any-s to the absolute minimum it's usually recommended to restrict that behaviour with another tsconfig.json setting:

{
"compilerOptions": {
"noImplicitAny": true
}
}

Unfortunately, we can’t put that kind of safety belt (that’d require an explicit typing) on a function return type. So if instead function fn(param): string { I'll forget that type (function fn(param) {), TypeScript will not keep an eye on what I return, or even if I return anything from that function. More precisely: it will infer the return value from whatever you’ve returned or not returned.

Thankfully that’s where TSLint helps. With the typedef rule you can make return types required:

{
"rules": {
"typedef": [
true,
"call-signature"
]

}
}

Which I’d argue is a good idea.

Tip 7. Type Guards

When you have a value that can be of several types, you have to take that into account in your algorithm to differentiate one type from the other. The thing about TypeScript is that it understands this logic.

type BookId = number | string;
function returnFormatterId(id: BookId) {
    return id.toUpperCase();
// 🛑 'toUpperCase' does not exist on type 'number'.
}
function returnFormatterId(id: BookId) {
    if (typeof id === 'string') {
// we've made sure it's a string:
return id.toUpperCase(); // so it's 👍
}
    // 👍 TS also understands that it
// has to be a number here:
return id.toFixed(2)
}

Tip 8. Once more about the generics

Let’s say we have this kind of quite general construct:

interface Bookmark {
id: string;
}
class BookmarksService {
items: Bookmark[] = [];
}

You want to use it in different applications, e.g. for storing Books or Movies.

In such application you could do something like:

interface Movie {
id: string;
name: string;
}
class SearchPageComponent {
movie: Movie;
constructor(private bs: BookmarksService) {}
    getFirstMovie() {
// 🛑 types are not assignable
this.movie = this.bs.items[0];
        // 👍 so you have to manually assert type:
this.movie = this.bs.items[0] as Movie;
}
    getSecondMovie() {
this.movie = this.bs.items[1] as Movie;
}
}

This kind of type assertion might be needed several times in that class.

What we can do instead is to define the BookmarksService class as a generic:

class BookmarksService<T> {
items: T[] = [];
}

Well, now it is too generic though… We want to assure that the types this class will work with will fulfil the Bookmark interface (i.e. have the id: string property). Here is the improved declaration:

class BookmarksService<T extends Bookmark> {
items: T[] = [];
}

Now in our SearchPageComponent we only need to specify the type once:

class SearchPageComponent {
movie: Movie;
constructor(private bs: BookmarksService<Movie>) {}
    getFirstMovie() {
this.movie = this.bs.items[0]; // 👍
}
    getSecondMovie() {
this.movie = this.bs.items[1]; // 👍
}
}

There is one additional improvement for that generic class that might be useful — if you are using it in other places in that general capacity and don’t want to write BookmarksService<Bookmark> is such cases.

You can provide a default type in the generic's definition:

class BookmarksService<T extends Bookmark = Bookmark> {
items: T[] = [];
}
const bs = new BookmarksService();
// I don't have to provide the type for the generic now
// - in that case 'bs' will be of that default type 'Bookmark'

Tip 9. Type your route params

This one is specifically for the Angular folks. I don’t think I can describe this better than I did in original tweet — so here it is:

Tip 10. Create Interfaces from API response

If you have a big, nested response from the API, it’s really tedious to type the corresponding interface(s) by hand. It’s a task that should be handled by machines! 🤖

There are couple of options:

..are some of them, but frankly, their servers are often not available. I just usually start with them because of very memorable urls :-) For the best results and some additional options too, use https://app.quicktype.io/

It also provides a handy Visual Studio Code plugin:

Tip 11. For more tips…

I share them pretty often on Twitter, so if you are interested in such things, follow me there 🤓

And as a final word, a big shout-out to Angular Academy Show — thank’s for hosting me! There are couple of interesting interviews there, e.g. if you’re into GraphQL or PWA… or other subjects, definitely pay them a visit :)