Notes on TypeScript: Recursive Type Aliases and Immutability
Tags: typescript, types
Published: 2020-01-24 13:56:40

---
title: Notes on TypeScript: Recursive Type Aliases and Immutability
published: true
description: Immutability
tags: typescript, types
series: Notes on TypeScript
---

### Introduction

These notes should help in better understanding `TypeScript` and might be helpful when needing to lookup up how to leverage TypeScript in a specific situation. All examples in this post are based on TypeScript 3.7.2.

---


### Recursive Types

Prior to the 3.7 release, it was was not possible to simply write:

```js
type Item = [string, number, Item[]];
```

The compiler would complain with: `Type alias 'Item' circularly references itself.`. This was suboptimal, as there are enough use cases where recursive types would be useful. On a side note it was possible for a type alias to reference itself via a property:

```js
type Item<T> = {
    value: T;
    reference: Item<T>;
}
```

There was still a way to achieve this, but it required to fallback to using `interface`. This is how the same type would have been defined before the 3.7 release:

```js
type Item = [string, number, Items[]];

interface Items extends Item {};
```

It required developers to switch back and forth between `type` and `interface` definitions and was more complicated than necessary. With 3.7 we can write:

```js
type Item = [string, number, Item[]];
```

This is a useful improvement.


---

### Immutability

Now that we learned about the recursive type aliases, let's create an immutable type that we can use to add more guarantees into our application code.

We might want to define a specific `Shape` type for example.

```js
type Shape = {
    color: string;
    configuration: {
        height: number;
        width: number;
    }
};
```

With the release of TypeScript 3.4, `const assertions` where introduced. This would enable us to make a JavaScript object immutable.

```js
const shape = {
    color: 'green',
    configuration: {
        height: 100,
        width: 100,
    }
} as const;
```

Which would result in:

```js

const shape: {
    readonly color: "green";
    readonly configuration: {
        readonly height: 100;
        readonly width: 100;
    }
};
```

`as const` converts the properties of any object to `readonly`, which would guarantee that our `shape` object is immutable.

```js
shape.color = 'blue'; 
// Error!  Cannot assign to 'color' because it is a read-only property.
shape.configuration.height = 101;
// Error! Cannot assign to 'height' because it is a read-only property.
```

But there are limitations with this approach. What if we had a property containing an array for example?

```js
const numberArray: number[] = [];
const shape = {
    color: 'green',
    attributes: numberArray,
    configuration: {
        height: 100,
        width: 100,
    }
} as const;

// This would work...
shape.attributes.push(1);
```

There is another limitation, for example when working with functions. See the next example.

```js
const transformShape = (shape: Shape) => {
    shape.configuration.height = 101;
    return shape;
}

// Would work...
transformShape(shape);
```

We might add the `Readonly` type to ensure that we can't change any values.

```js
const transformShape = (shape: Readonly<Shape>) => {
    shape.configuration.height = 101;
    return shape;
}

// Would work...
transformShape(shape);
```

From the above example we can note that `Readonly` would only help us if we tried to change any top level properties.

```js
const transformShape = (shape: Readonly<Shape>) => {
    shape.color = 'red';
    // Error! Cannot assign to 'color' because it is a read-only property.
    return shape;
}
```

The `Readonly` type is defined like the following:

```js
type Readonly<T> = { readonly [P in keyof T]: T[P]; }
```
The problem is that it doesn't work with deep nested structures.

To work around the issue we can build our own `MakeReadOnly` type, that should ensure we can't mutate any deeply nested properties inside a function body.

As we learned about recursive type aliases just before, we can now create an immutable type definition.

```js
type MakeReadOnly<Type> = {
  readonly [Key in keyof Type]: MakeReadOnly<Type[Key]>;
};
```

Now that we have our own type definition let's see how we can apply this as compared to the previous examples.


```js
const shape: MakeReadOnly<Shape> = {
    color: 'green',
    configuration: {
        height: 100,
        width: 100,
    }
};

shape.color = 'blue';
// Error! Cannot assign to 'color' because it is a read-only property.
shape.configuration.height = 101;
// Error! Cannot assign to 'height' because it is a read-only property.
```

We get the same results like in the previous examples, using `const assertions`. This might not be adding any additional value. But let's see what we gain by using our newly defined `MakeReadOnly` type when working with functions.

```js
const transformShape = (shape: MakeReadOnly<Shape>) => {
    shape.color = 'red';
    // Error! Cannot assign to 'color' because it is a read-only property.
    shape.configuration.height = 101;
    //Error! Cannot assign to 'height' because it is a read-only property
    return shape;
}
```

As we can see from the above example, it's not possible to change the values of any nested properties anymore. This gives us more control on how objects are handled inside function bodies.

In summary there is no concise way to guarantee immutability in TypeScript. Some additional work is needed to ensure that at least in specific parts of the application mutating an object or array is limited.


### Links

[Official Release Notes on Recursive Type Aliases](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#more-recursive-type-aliases)

[Offical Release Notes on const assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions)

[Marius Schulz: Const Assertions in Literal Expressions in TypeScript](https://mariusschulz.com/blog/const-assertions-in-literal-expressions-in-typescript)

If you have any questions or feedback please leave a comment here or connect via Twitter: [A. Sharif](https://twitter.com/sharifsbeat)