TypeScript - #02 - Readonly
Tags: typescript, spanish
Published: 2022-08-06 06:15:00

En otros idiomas: [Inglés](https://dev.to/devjosemanuel/typescript-02-readonly-4pid)
Enlace al desafío: [Readonly](https://github.com/type-challenges/type-challenges/blob/main/questions/00007-easy-readonly/README.md)

---

En este desafío lo que perseguiremos es construir es la utilidad `Readonly<T>` que nos ofrece TypeScript pero sin utilizarla lo que implica que lo que vamos a construir es un tipo que nos retornará todos los atributos del tipo `T` que han sido establecidos como `readonly` lo que viene a indicar que estos atributos no van a poder ser reasignados (no se podrá cambiar su valor).

A modo de ejemplo:

```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.
```

---

## Solución

```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]
}
```

---

## Explicación

Lo primero que tenemos que pensar para construir nuestra solución es que la asignación de modificador de visibilidad `readonly` solamente debería aplicarse sobre aquellos atributos de primer nivel del tipo de datos sobre el que trabajará nuestra utilidad. ¿Qué quiere esto decir? Pues que si ampliamos la declaración de la interfaz `Todo` que tenemos en el ejemplo:

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

el modificador `readonly` únicamente deberá aplicarse a los atributos de primer nivel (`title`, `description`, `completed` y `meta`) pero no a los de segundo nivel y posteriores (lo que viene a decir que el atributo `author` del objeto `meta` no tienen que tenerlo). Es decir, que lo que tenemos que obtener es un tipo de datos como el siguiente:

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

Para obtener la solución lo primero que tenemos que pensar es que de alguna manera lo que se nos está pidiendo es que nuestra utilidad lo que haga es crear un nuevo objeto a partir de uno de partida por lo que siempre que nos enfrentemos a un desafío de este tipo la mejor idea de afrontarlo es pensar en una mapeo de las propiedades del objeto de partida en las propiedades del objeto con la solución.

Así pues como paso inicial establecemos que el resultado de aplicar nuestro tipo va a ser un nuevo objeto:

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

¿Y cómo podemos recorrer todas las propiedades que están recogidas en el objeto `T`? Pues en primer lugar tendremos que obtenerlas cosa que logramos gracias al uso del operador `keyof` sobre el objeto `T`. En el ejemplo con el que estamos trabajando tendremos algo como lo siguiente:

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

y ahora vamos a tener que recorrerlas como si estuviésemos recorriendo un array de JavaScript para lo cual nos apoyaremos en el operador `in` como sigue:

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

Ahora que ya sabemos cómo obtener todos los atributos del objeto `T` la siguiente que tenemos que hacer es crear un mapped type es decir, tipo que se obtiene mapeando los datos de otro tipo. En nuestro caso lo que nosotros queremos es recorrer crear un nuevo objeto donde cada uno de sus atributos sea el mismo que el objeto `T` de partida por lo que escribiríamos algo como lo siguiente:

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

¿Y qué valor le asignaremos a cada uno de estos atributos? Pues en principio el mismo que tiene el objeto original cosa que obtenemos accediendo directamente a dicho atributo en `T`:

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

¿Qué hemos logrado hasta aquí? Pues la verdad es que muy poco ya que lo único que está haciendo la declaración anterior es devolvernos exactamente el mismo tipo `T` con el que estamos trabajando.

Nos quedará añadir el modificador `readonly` para poder lograr el objetivo que estamos persiguiendo para lo cual tendremos varias posibilidades. La primera de ellas consistiría en añadirlo a la parte que recoge el valor del nuevo tipo de datos:

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

pero aquí el problema radica en que TypeScript nos dará un error porque `readonly` es un modificador que solamente se puede aplicar sobre arrays o tuplas y nosotros no tenemos la certeza de que `T[K]` vaya a ser un array o tupla.

La otra posibilidad que tenemos es añadirlo antes de la declaración de cada uno de los atributos de nuestro nuevo objeto:

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

Y con esto lograríamos el resultado que estamos buscando.

### Eliminar el atributo readonly

Ahora que sabemos cómo añadir el atributo `readonly` simplemente mencionar que TypeScript nos ofrece la posibilidad de utilizar el operador `-` delante de este modificar y lo que hará será quitarlo de todos aquellos atributos que lo puedan tener en el objeto `T`.

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

### Un nivel de profundidad adicional

Podríamos irnos un nivel de profundidad en la declaración de los atributos sobre los que hay que eliminar el modificador `readonly` siendo este un aspecto interesante al que merece la pena dedicarle unos instantes.

El primero paso para lograrlo consistirá en utilizar un tipo condicional para determinar si el valor que tiene asignado el atributo con el que estamos trabajando es un objeto o no:

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

Y aquí es donde entra en juego el conocer que TypeScript nos va a permitir hacer recursivas a los tipos de datos que estamos definiendo de forma análoga a como se hacen llamadas a funciones recursivas en JavaScript. ¿Qué quiere esto decir? Pues que en nuestro caso en el caso de que se cumpla la condición lo que haremos será volver a aplicar `MyReadonly` pero en este caso pasándole el objeto que está asociado al atributo `K` del objeto `T` de partida:

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

y en el caso de que no se cumpla la condición (es decir, que el valor que tienen asignado el atributo `K` no sea un objeto) lo que retornaremos será dicho valor:

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

---

## Enlaces de interés

- [`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)