5 TypeScript tips to improve your architecture

nehalist

Kevin

Posted on August 26, 2019

5 TypeScript tips to improve your architecture

5 TypeScript tips to improve your architecture

Even though I work pretty much everyday with TypeScript there's something new I learn about frequently. I really enjoy working with it and try to get the most out of it whenever possible - and there's one thing for sure: TypeScript can do a lot of things.

So here are some things I've learned through time which helped me to improve my applications and - ultimately - become a better TypeScript developer.

Utility types

TypeScript provides some useful types right out of the box - they're called utility types and can be found here.

One of my personal favorites is the Readonly<T> type makes all properties of T readonly, making it easy to help you to maintain immutability.

Another handy type is Required<T> which makes all properties required.

There's even more with things like Pick<T, K> which creates a type of picked properties from another type, NonNullable<T> which removes null and undefined from properties, Exclude<T, U> which removes properties from a type and even more - go check them out, they're really useful.

Use unknown instead of any

A lot of times if you don't have exact type information you might just go for any. And that was actually totally fine - until TypeScript 3.0 introduced the new unknown type, the "type-safe counterpart of any" (as described by TypeScript).

Just like the any type you can assign whatever you want to the unknown type - but you can't do _any_thing with it like the any type allows for:

const imUnknown: unknown = "hello there";
const imAny: any = "hello there";

imUnknown.toUpperCase(); // error: object is of type "unknown"
imAny.toUpperCase(); // totally fine

Hence unknown prevents calling any method on it due to its unknown nature. Additionally assigning unknown types to other types is not possible either:

let a: unknown = "hello world";
let b: string;

b = a; // error: a is unknown hence it's not possible to assign it to `string`

Often times the variable you just declared any is actually unknown - which prevents faulty assignments or calling methods on it without knowing what methods are provided by it.

See here for further information regarding the unknown type.

Lookup types

Lookup types are a neat way to help you finding what you're looking for. Let's assume you've got an object with a dynamic number of properties and want to get a specific property out of it - it's highly likely to do it the following way:

const persons = {
  john: { surname: 'Doe' },
  bob: { surname: 'The builder' }
};

persons.john; // equals { surname: 'Doe' }

But there's one problem: what if the john property does not exist? Your application is likely to tragically die - but that could have been prevented by TypeScript!

By implementing a very small helper we're able to prevent this:

function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key]; // Inferred type is T[K]
}

By using this helper we can assure that the property exists when using it:

function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key]; // Inferred type is T[K]
}

const persons = {
    john: { surname: 'Doe' },
    bob: { surname: 'The builder' }
};

getProperty(persons, 'bob');
getProperty(persons, 'john');
getProperty(persons, 'someoneelse'); // Error: Argument of type "someonelse" is not assignable to parameter of type "bob" | "john"

It may be common to take use of that within classes which makes the obj param obsolete. It might be implemented as a getter for getting a specific item where we want to know which elements are available. In this case we're going to use a fairly uncommon syntax which:

class PersonList {
  persons = {
    john: { surname: 'Doe' },
    bob: { surname: 'The Builder' }
  }

  getPerson(name: keyof PersonList['persons']) {
    return this.persons[name];
  }
}

keyof PersonList['persons'] is something you probably don't see everyday.

Which would cause calling personList.getPerson('john') to be perfectly fine, while personList.getPerson('jane') would fail since there's no jane key available.

Extending third-party libraries

If you've ever wanted to extend a third party library but were limited by the public API of that library module augmentation is what you're looking for. But when do you want that?

Let's say we've got an Angular application and we want to name our routes. The Angular Route interface doesn't provide a name property by default - that's were module augmentation comes in handy.

By creating a typings.d.ts in the root of our Angular project we can simply tell TypeScript that we want to have an additional, optional property which stores the name:

import {Route} from '@angular/router';

declare module '@angular/router' {
  interface Route {
    name?: string;
  }
}

It's important to import from @angular/router so that TypeScript registers the remaining properties.

Now we can simply add a name to our routes:

const routes: Routes = [
  {
    path: '',
    name: 'theMightyDashboard',
    component: DashboardComponent
  },
];

Which - for example - can be used within our components by injecting ActivatedRoute:

constructor(private route: ActivatedRoute) {
  console.log(route.routeConfig.name); // logs `theMightyDashboard`!
}

ES types

Often times I see TypeScript code where most things are properly typed, except native ES elements. Think of something like Maps, Promises, Arrays and all that kind of stuff. But TypeScript provide types for these objects right out of the box;

Maps for example do provide generics to define the contents of our map. So instead of

const map = new Map();
map.set('name', 'John Doe');

We can improve this by specifying the types of our map key and value:

const map: Map<string, string> = new Map();
map.set('name', 'John Doe');

This prevents us from accidentally adding a type which is not allowed:

const map: Map<string, string> = new Map();
map.set('name', 123); // error: 123 is not a string

The same applies to Set:

const set: Set<number> = new Set();
set.add(123); // Ok!
set.add('whatever'); // Type error

Or native arrays:

const elements: Array<{ name: string }> = [];
elements.push({ name: 'John Doe' }); // Ok!
elements.push('John Doe'); // Type error

If you want to take a look at all types available see the official typings for ES5 and ES6 (or all the other specs)

Anything else?

As said in the beginning TypeScript can do so many things that it can get difficult to stay up to date at sometimes. If there's something you really like about TypeScript which you don't see everyday please let me know in the comments!


If you liked this post feel free to leave a ❤, follow me on Twitter and subscribe to my newsletter. This post was originally published at nehalist.io on August 06, 2019.

💖 💪 🙅 🚩
nehalist
Kevin

Posted on August 26, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related