En otros idiomas: Inglés
Enlace al desafío: Readonly
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:
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
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:
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:
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:
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:
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:
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:
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
:
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:
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:
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
.
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:
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:
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:
type MyReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? MyReadonly<T[K]>
: T[K]
}
Top comments (0)