TypeScript - #02 - Readonly
Tags: typescript
Published: 2022-08-06 06:16:00

Other languages: [Spanish](https://dev.to/devjosemanuel/typescript-02-readonly-4kea)
Link to the challenge: [Readonly](https://github.com/type-challenges/type-challenges/blob/main/questions/00007-easy-readonly/README.md)

---

In this challenge what we will pursue is to build the `Readonly<T>` utility offered by TypeScript but without using it, which implies that what we are going to build is a type that will return all the attributes of type `T` that have been set as `readonly`, which means that these attributes cannot be reassigned (it will not be possible to change their value).

As an example:

```tsx
interface Todo {
	title: string
	description: string
}

const todo: MyReadonly<Todo> = {
	title: 'Hey',
	description: 'foobar'
}

todo.title = 'Hello' // Error: cannot reassign a readonly property.
todo.description = 'barFoo' // Error: cannot reassign a readonly property.
```

---

## Solution

```tsx
type MyReadonly<T> = {
	readonly [K in keyof T]: T[K]
}

// If we want to remove de readonly we can do
type MyReadonly<T> = {
	-readonly [K in keyof T]: T[K]
}
```

---

## Explanation

The first thing we have to think about in order to build our solution is that the `readonly` visibility modifier assignment should only be applied on those first level attributes of the data type on which our utility will work. What does this mean? Well, if we extend the `All` interface declaration we have in the example:

```tsx
interface Todo {
	title: string
	description: string
	completed: boolean
	meta: {
		author: string
	}
}
```

the `readonly` modifier should only be applied to the first level attributes (`title`, `description`, `completed` and `meta`) but not to the second level and later ones (which means that the `author` attribute of the `meta` object does not have to have it). That is, what we have to obtain is a data type like the following:

```tsx
interface Todo {
	readonly title: string
	readonly description: string
	readonly completed: boolean
	readonly meta: {
		author: string
	}
}
```

To obtain the solution the first thing we have to think is that somehow what we are being asked is that our utility creates a new object from a starting one, so whenever we face a challenge of this type the best idea to face it is to think of a mapping of the properties of the starting object in the properties of the object with the solution.

So as an initial step we establish that the result of applying our type is going to be a new object:

```tsx
type MyReadonly<T> = {}
```

And how can we go through all the properties that are collected in the `T` object? Well, first of all we will have to obtain them, which we achieve thanks to the use of the `keyof` operator on the object `T`. In the example we are working with we will have something like the following:

```tsx
keyof Todo --> 'title' | 'description' | 'completed' | 'meta'
```

and now we are going to have to traverse them as if we were traversing a JavaScript array for which we will rely on the `in` operator as follows:

```tsx
K in keyof Todo --> ['title', 'description', 'completed', 'meta']
```

Now that we know how to obtain all the attributes of the object `T` the next thing we have to do is to create a mapped type, that is to say, a type that is obtained by mapping the data of another type. In our case what we want is to create a new object where each of its attributes is the same as the starting `T` object so we would write something like the following:

```tsx
type MyReadonly<T> = {
	[K in keyof T]: ...
}
```

And what value will we assign to each of these attributes? Well, in principle the same as the original object, which we obtain by accessing directly to that attribute in `T`:

```tsx
type MyReadonly<T> = {
	[K in keyof T]: T[K]
}
```

What have we achieved so far? Well, the truth is that very little since the only thing that the previous declaration is doing is to return us exactly the same type `T` with which we are working.

We will have to add the modifier `readonly` to be able to achieve the objective that we are pursuing for which we will have several possibilities. The first of them would consist of adding it to the part that collects the value of the new data type:

```tsx
type MyReadonly<T> = {
	[K in keyof T]: readonly T[K]
}
```

but here the problem lies in that TypeScript will give us an error because `readonly` is a modifier that can only be applied on arrays or tuples and we do not have the certainty that `T[K]` is going to be an array or tuple.

The other possibility we have is to add it before the declaration of each of the attributes of our new object:

```tsx
type MyReadonly<T> = {
	readonly [K in keyof T]: T[K]
}
```

And with this we would achieve the result we are looking for.

### Remove the readonly attribute

Now that we know how to add the `readonly` attribute, just mention that TypeScript offers us the possibility of using the `-` operator in front of this modify and what it will do is to remove it from all those attributes that may have it in the `T` object.

```tsx
type MyReadonly<T> = {
	-readonly [K in keyof T]: T[K]
}
```

### An additional level of depth

We could go one level deeper in the declaration of the attributes on which the `readonly` modifier has to be removed, being this an interesting aspect to which it is worth dedicating a few moments.

The first step to achieve this is to use a conditional type to determine whether the value assigned to the attribute we are working with is an object or not:

```tsx
type MyReadonly<T> = {
	readonly [K in keyof T]: T[K] extends object ? ... : ...
}
```

And here is where it comes into play to know that TypeScript will allow us to make recursive to the data types that we are defining in an analogous way to how recursive function calls are made in JavaScript. What does this mean? Well, in our case, if the condition is fulfilled, what we will do is to apply `MyReadonly` again, but in this case passing the object that is associated to the `K` attribute of the starting `T` object:

```tsx
type MyReadonly<T> = {
	readonly [K in keyof T]: T[K] extends object
		? MyReadonly<T[K]>
		: ...
}
```

and in the case that the condition is not fulfilled (that is to say, that the value that has assigned the attribute `K` is not an object) what we will return will be this value:

```tsx
type MyReadonly<T> = {
	readonly [K in keyof T]: T[K] extends object
		? MyReadonly<T[K]>
		: T[K]
}
```

---

## Links of interest

- [`extends` Conditional Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#handbook-content)
- [`in` Operator](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-in-operator-narrowing)
- [`keyof` Type Operator](https://www.typescriptlang.org/docs/handbook/2/keyof-types.html#handbook-content)
- [Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#handbook-content)
- [`Readonly` Utility Type](https://www.typescriptlang.org/docs/handbook/utility-types.html#readonlytype)