TypeScript - #01 - Pick
Tags: typescript, spanish
Published: 2022-07-30 06:46:00

En otros idiomas: [Inglés](https://dev.to/devjosemanuel/typescript-01-pick-3chi)
Enlace al desafío: [Pick](https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.md)

---

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:

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

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
	title: 'Clean room',
	completed: false
}
```

---

## Solución

```tsx
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`:

```tsx
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:

```tsx
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 que `title` es uno de los atributos que están recogidos dentro de la interfaz `Todo`.
- En el segundo caso tampoco deberíamos tener un error puesto que el **union type** que está formado por `title` y `completed` se corresponde con dos de los atributos que están definidos en `MyPick`.
- 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 que `MyPick` 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.

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:

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

```tsx
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:

```tsx
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:

```tsx
MyPick<T, K extends keyof T> = {
	[P in K]: T[P]
}
```

Si volvemos a nuestro ejemplo obtendríamos algo como lo siguiente:

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

---

### 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)
- [`Pick` Utility Type](https://github.com/type-challenges/type-challenges/blob/main/questions/00004-easy-pick/README.md)