Julian​Garamendy​.dev

Readonly<T> and Better Error Messages

5 February, 2020

A few weeks ago I learned something about TypeScript errors and utility types.

The following is true in TypeScript v3.7.5. In my experience, error messages in TS improve a lot with each release, so this may soon be irrelevant.

As usual, here's an example with bananas.

I had this Banana type:

type Banana = {
  id: number;
  name: string;
  color: number;
  weight?: number;
  length?: number;
  thumbnail?: string;
  pictures?: Array<string>;
};

I was trying to be clever and I thought I would make my type immutable by using the Readonly utility type like this:

type Banana = Readonly<{
  id: number;
  name: string;
  color: number;
  weight?: number;
  length?: number;
  thumbnails?: string;
  pictures?: ReadonlyArray<string>;
}>;

But this turned out not to be such a great idea.

I had made a (Readonly)Map with a few bananas:

const bananaMap: ReadonlyMap<number, Banana> = new Map([
  [1, { id: 1, name: "yellow banana", color: 0xffff00 }],
  [2, { id: 2, name: "red banana", color: 0xff0000 }],
  [3, { id: 3, name: "green banana", color: 0x00ff00 }]
]);

Then I tried to access an element in this way:

const banana: Banana = bananaMap.get(1);

And I got an error on the banana identifier which looked like this:

Type 'Readonly<{ id: number; name: string; color: number; weight?: number 
| undefined; length?: number | undefined; thumbnails?: string | undefined;
 pictures?: readonly string[] | undefined; }> | undefined' is not 
assignable to type 'Readonly<{ id: number; name: string; color: number; 
weight?: number | undefined; length?: number | undefined; thumbnails?: 
string | undefined; pictures?: readonly string[] | undefined; }>'.
  Type 'undefined' is not assignable to type 'Readonly<{ id: number; 
name: string; color: number; weight?: number | undefined; length?: 
number | undefined; thumbnails?: string | undefined; pictures?: 
readonly string[] | undefined; }>'.

It took me a while to understand what the problem was. The important part was on the second "paragraph" and I had to scroll down to find it, and still stare at it for a while.

You can see this example in the TypeScript Playground

So I changed the type, marking each field as readonly instead:

type Banana = {
  readonly id: number;
  readonly name: string;
  readonly color: number;
  readonly weight?: number;
  readonly length?: number;
  readonly thumbnails?: string;
  readonly pictures?: ReadonlyArray<string>;
};

Then the error message looked a bit better:

Type 'Banana | undefined' is not assignable to type 'Banana'.
  Type 'undefined' is not assignable to type 'Banana'.

And the issue was evident! The get method in the Map class returns an element or undefined. That means we can't annotate const banana with the Banana type.

const banana: Banana = bananaMap.get(1); // ❌ error!

Some possible fixes:

// 1
const banana: Banana | undefined = bananaMap.get(1); // ✅ no error

// 2
const banana: Banana = bananaMap.get(1)!; // ⚠️ no error, but (*)

* I would avoid the non-null assertion operator when possible.

You can see this new example in the TypeScript Playground.

UPDATE:

My friend Albert pointed our that we can get the "nice and short" error message mentioning out Banana type if we use interface instead of type:

interface Banana extends Readonly<{
  id: number;
  name: string;
  color: number;
  weight?: number;
  length?: number;
  thumbnails?: string;
  pictures?: ReadonlyArray<string>;
}> { }

You can see this last example in TypeScript Playground


I would love to hear what you use to declare immutability in your code.
Please comment!


Photo by v2osk on Unsplash


Comment on dev.to: https://dev.to/juliang/readonly-t-and-better-error-messages-3i9l