Why TypeScripts never type is super useful
Tags: typescript, functional, webdev, refactorit
Published: 2022-02-12 07:29:55

At first glance, the [never](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-never-type) type  does not sound very useful for everyday coding. But actually, the following property of the type comes in very handy:

> […] The never type is assignable to every type; however, no type is assignable to never (except never itself) […]

With a small hack, this can be used to increase the type safety of a code base with no risk of breaking existing code and without any runtime overhead.

## Problem
Maybe you have lots of places in your code, where you use some data like an userId, an email, a firstname or whatever. You could just use the standard types for that (string and so on):

```typescript
getUserByUserId(userId: string): Observable<User>;
```

But this approach has some problems:
* You could accidentally call this method with the wrong parameters, e.g. with an email instead of an userId.
* This error would only be found during runtime, not compile time.
* Sometimes during [refactoring](https://refactoring.com/) your codebase you want to find all places where a field like a userId is used.  This is very hard if it has a simple type, but very easy if it has a custom type.

## Solution

The problems described above disappears, if we don’t use the built in string type but create a custom type for the userId:

```typescript
getUserByUserId(userId: UserId): Observable<User>;
```

Now it’s easy to find all places where this UserId type is used. And is not possible to accidentally call this method with an email because the latter has another type.

## Where does the never type come into play?

How to implement this new UserId type? The naive solution does not work out:

```typescript
export type UserId = string;
getUserByUserId('some string'); // no compiler errors here :-(
```

The problem is that for typescript `string` and `UserId` are the compatible (because at runtime they are the same). We need to change the `UserId` type, so that they are (at least at compile time) not compatible anymore in order to get compiler errors. We could create a new `UserId` class to achieve that. But this might break existing code, which you forgot to migrate and which still expects to get a string, at runtime. But with the help of the [never](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-never-type) type you can define this new type like this:

```typescript
export type UserId = string & {
  // name of the following field needs to be unique. If it 
  // were not unique and you would reuse it for another type, 
  // both types would be compatible!
  ____doesNotMatter_UserId: never;
};

// you can't create new values of type UserId by hand, because 
// you cannot assign anything to never. so you'll need this
// converter function:
export const toUserId = (userId: string) => userId as UserId;
```

This solution has some nice characteristics:
* It’s not possible to assign string values to variables of type `UserId` anymore, because they are missing the `____doesNotMatter_UserId` attribute. The compiler will complain about that if you give it a try.
* To convert a string to the new type, you need to cast latter to it. Creating a value of this type another way is impossible because you cannot assign anything to never. Normally you’ll have to do this in your tests, during url parameter parsing or (maybe) when converting your [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) resources.
* I write a factory method like this for the casting normally: export `const toUserId = (userId: string) => userId as UserId;`. This factory method is nice, because now you can search for the places where `UserId` values are created.
* A big advantage is that this trick does not change the runtime behavior of your code. The sanity check is done only during compile time. Because it’s still a `string` during runtime, nothing bad will happen if you forget to migrate a place in your code (and which expects a `string` in our example above).
* It can be used for other types too, not only strings.

So let’s see this type in action:

```typescript
export type UserId = string & {
  // name of the following field needs to be unique. If it 
  // were not unique and you would reuse it for another type, 
  // both types would be compatible!
  ____doesNotMatter_UserId: never;
};

// you can't create new values of type UserId, so you'll need 
// this converter function:
export const toUserId = (userId: string) => userId as UserId;

// an example function which expects our new type
export const isSuperUser = (userId: UserId) => 
                                   userId === 'superuser';

// a function we forgot to migrate and which still get's a string
export const unmigratedIsGuestUserFn = (userId: string) 
                                       => userId === 'guest';

// some examples
const aUserId: UserId = toUserId('u1111');

isSuperUser(aUserId); // works and return false

isSuperUser('superuser'); // Compile time error: Argument of type
 // 'string' is not assignable to parameter of type 'UserId'. 
 // Type 'string' is not assignable to type '{ 
 // ____doesNotMatter_UserId: never; }'.

unmigratedIsGuestUserFn(aUserId); // no compile time errors 
// because at runtime our new type is just a string :-). 
// So this type refactoring is very safe

// warning - don't reuse your ___doesNotMatter attributes:
export type Email = string & {
  // DO NOT DO THIS:
  ____doesNotMatter_UserId: never;
};

//  Email and UserId are compatible because they have the same
//  signature, so we don't get compiler errors here:
const email: Email = toUserId('superuser')
isSuperUser(email);

// So -> always use unique names for these 
// ____doesNotMatter attributes
```

## Conclusion
I found this approach to be especially useful, if values of a type are used in many places but are created only in few cases. For instance, I expect that I would only need to call the  factory function toUserId to convert strings to the new UserId type during:
* converting a rest resource to our domain model (should be one place)
* parsing a url parameter (should be few places)
* in tests
But there might be lots of places, were a function gets a value our new type as parameter and just passes it around. So through this approach I get the former described benefits of a richer type model.

## Acknowledgements
I learned about this trick during an [Angular](https://angular.io/) meetup held by Thiele Leonard in 2020. I found a [recording of the same „Angular at scale“ talk](https://www.youtube.com/watch?v=9yd0iha2F6M) hold on a different occasion, which contains many more tricks, so have a look.