En otros idiomas: Inglés
Enlace al desafío: Pick
En este desafío lo que perseguiremos es construir el tipo Pick<T, K>
sin hacer uso del mismo. Deberemos construir un tipo de datos que nos permitirá extraer el conjunto de propiedades K
de un tipo T
. Por ejemplo:
interface Todo {
title: "string"
description: "string"
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: "'Clean room',"
completed: false
}
Solución
MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
Explicación
Vamos a partir de la definición básica del tipo de datos que queremos construir donde estableceremos los parámetros que esperamos que reciba y supondremos además que inicialmente retornará any
:
type MyPick<T, K> = any
Pero antes de continuar vamos a detenernos unos instantes para entender bien cómo ha de funcionar MyPick
. Para ello nos vamos a apoyar en la declaración de la interfaz Todo
de la definición del challenge presentando tres escenarios:
MyPick<Todo, 'title'>
MyPick<Todo, 'title' | 'completed'>
MyPick<Todo, 'title' | 'completed' | 'invalid'>
¿Cómo ha de comportarse MyPick
en cada una de los tres escenarios anteriores?
- En el primero de los casos
MyPick
no debería dar un error puesto quetitle
es uno de los atributos que están recogidos dentro de la interfazTodo
. - En el segundo caso tampoco deberíamos tener un error puesto que el union type que está formado por
title
ycompleted
se corresponde con dos de los atributos que están definidos enMyPick
. - El tercer caso es diferente puesto que estamos nuevamente frente a un union type pero en este caso la particularidad es que uno de los valores que lo forman no se corresponde con uno de los atributos de la interfaz
Todo
por lo queMyPick
debería informarnos de ello con un mensaje de error.
Con esto en mente parece que la clave para poder resolver nuestro problema está en restringir de alguna manera el conjunto de los valores que podemos escribir en el segundo de los parámetros de MyPick<T, K>
o, dicho de otra manera, definir K
de tal manera que solamente pueda aceptar valores que se corresponden con los atributos de T
.
keyof
Ahora bien ¿cómo podemos decir en TypeScript que queremos extraer el conjunto de los atributos que están asociados con un tipo? La respuesta es gracias al uso del operador keyof
.
Para entenderlo mejor con nuestro ejemplo, si aplicamos este operador a la interfaz Todo
lo que vamos a obtener es un union type formado por el nombre de todos los atributos que están definidos dentro de la interfaz.
keyof Todo ---> 'title' | 'description' | 'completed'
extends
Ahora que ya tenemos el tipo de datos formado por todos los atributos de la interfaz tenemos que restringir de alguna manera que solamente se puedan elegir entre uno de ellos y aquí es donde entran en juego los conditional types.
Pensando en términos de teoría de conjuntos:
- ¿Qué es lo que queremos probar? Que el conjunto formado por los tipos de datos que está asociados a todos los atributos que queremos extraer es un subconjunto del conjunto formado por los atributos de tipo con el que estamos trabajando.
- ¿TypeScript nos ofrece una mecanismo para poder realizar esta comprobación? Sí, y es el uso de los conditional types o, en nuestro caso, del operador
extends
.
Como podemos intuir extends
es un operador que nos va a permitir realizar comprobaciones de forma similar a como las realizamos JavaScript:
A extends B ---> true siempre que A sea un subconjunto de los elementos de B.
A extends B ---> false en caso contrario.
Plasmemos esta idea en los escenarios que hemos mostrado anteriormente:
B = keyof Todo ---> 'title' | 'description' | 'completed'
'title' extends B --> true
'title' | 'completed' extends B --> true
'title' | 'completed' | 'invalid' extends B --> false
Uniendo todas las piezas
Ya solamente nos quedan unir las dos piezas anteriores para lograr nuestro objetivo estableciendo que el conjunto de los valores que se pueden establecer en el parámetro K
ha de pertenecer al conjunto de los atributos que está recogidos en el tipo T
. Esto nos deja con lo siguiente:
MyPick<T, K extends keyof T> = any
Retornando el valor
En la declaración anterior estamos viendo que el resultado que se está devolviendo es any
y esto no es lo que se persigue en el desafío puesto que lo que queremos es retornar un objeto que contenga únicamente los atributos de T
que hemos especificado en el parámetro K
.
La forma de lograrlo es utilizando lo que se conoce como un Mapped Type que no es más que construcción de un nuevo tipo a partir de la definición de otro.
Vamos a comenzar definiendo el mapped type más sencillo que se nos puede ocurrir para lograr nuestro objetivo que sería aquel que siempre devolviese un tipo sin atributos como resultado:
MyPick<T, K extends keyof T> = {}
¿Cómo definimos ahora los atributos de este objeto? Pues aquí es donde tenemos que entender que tanto T
como K
son dos tipos que estarán disponibles durante la declaración del tipo que estamos construyendo (en cierta medida, si pensamos en términos de JavaScript, es como si ambos tipos estuviesen vinculados al scope de la declaración de nuestro nuevo tipo) y que, además, para la definición de los atributos nos interesará el conjunto que está recogido en K
.
¿Existe una posibilidad de recorrer todos los valores recogidos en K
en TypeScript? La respuesta es que sí y es gracias al uso del operador in
. Pensando en términos de arrays podíamos hacernos un esquema mental como el siguiente:
K ---> 'title' | 'description' | 'completed'
P in K ---> ['title', 'description', 'completed']
donde P
es un tipo que pasará a tener los valores title
, description
y completed
respectivamente mientras se está recorriendo el array.
¿Y podemos definir un atributo dentro del mapped type a partir de cada uno de estos elementos del array? La respuesta nuevamente es que sí utilizando para ello la notación entre []
donde
MyPick<T, K extends keyof T> = {
[P in K]: ...
}
Esto que de palabra es complejo de explicar se ve mejor en el ejemplo que estamos siguiendo:
interface Todo {
title: string
description: string
completed: boolean
}
MyPick<Todo, 'title'> ---> { title: ... }
MyPick<Todo, 'title' | 'description'> ---> { title: ..., description: ... }
MyPick<Todo, 'title' | 'description' | 'completed'> ---> { title: ..., description: ..., completed: ... }
Ya solamente nos queda por asignar el tipo de datos que está asociado a cada uno de los atributos que estamos extrayendo y esto se consigue de forma muy sencilla sin más que utilizar el operador []
sobre el tipo de datos indicando cuál es el elemento del que queremos extraer la información del tipo (de forma análoga a cómo podemos hacerlo en TypeScript para acceder a cada uno de los elementos de un objeto).
Pero ¿cómo sabemos cuál es el atributo concreto? Pues así tenemos que volver a recordar que todos los tipos genéricos que vayamos utilizando durante la declaración de nuestro tipo pueden ser utilizados en el resto de su definición.
En nuestro caso, como hemos declarado algo como [P in K]
para recorrer todos los atributos que están recogidos dentro del conjunto de las claves a extraer K
y como además tenemos la certeza de que estas claves pertenecerán al tipo sobre el que estamos trabajando T
ya simplemente podemos utilizar T[P]
para obtener el tipo asociado a dicho atributo:
MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
Si volvemos a nuestro ejemplo obtendríamos algo como lo siguiente:
interface Todo {
title: string
description: string
completed: boolean
}
MyPick<Todo, 'title'>
---> { title: string }
MyPick<Todo, 'title' | 'description'>
---> { title: string, description: string }
MyPick<Todo, 'title' | 'description' | 'completed'>
---> { title: string, description: string, completed: boolean }
Top comments (0)