Convertir un array de objetos a un objeto usando TypeScript
Tags: typescript, tutorial, spanish, javascript
Published: 2021-08-03 12:27:03

Tuve la necesidad de convertir un arreglo de objetos (`Array<{ id: string, name: string }>`) en un sólo objeto donde la llave era el campo `id` y el valor era el campo `name`. Esto puede parecer muy sencillo al inicio, y lo es, pero a la hora de tipar correctamente el resultado en TypeScript me demoré un buen tiempo investigando hasta lograr encontrar la respuesta.

> **Advertencia:** Usaré el anglicismo "tipar" y "tipado" para referirme a la acción de crear los tipos de variables en TypeScript. Esta palabra no existe en Español.

## Función sin tipar

Queremos crear una función que haga la siguiente conversión:

```typescript
arrayCollectionToObject([
  { id: 'A', name: 'First' },
  { id: 'B', name: 'Second' },
  { id: 'C', name: 'Third' }
]); // { A: 'First', B: 'Second', C: 'Third' }
```

Comencemos con escribir la función que realizaría esta acción sin hacer uso alguno de tipos. La función se vería más o menos así:

```typescript
function arrayCollectionToObject(collection) {
  const result = {};
  for (const item of collection) {
    result[item.id] = item.name;
  }
  return result;
}
```

> Nota: Para disminuir un poco la complejidad del código en el ejemplo he usado una [aproximación imperativa](https://cosasdigitales.com/articulos-diseno-web/javascript-programacion-declarativa-vs-imperativa/) en lugar de usar una aproximación declarativa haciendo uso del [reduce](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce).

Vamos a describir que hace la función línea a línea.

```typescript
const result = {};
```

En esta línea simplemente estamos creando un nuevo objeto, este será el objeto en el cual realizaremos las operaciones de conversión del array.

```typescript
for (const item of collection) {
  result[item.id] = item.name;
}
```

Aquí estamos iterando uno a uno los elementos que están en el array, haciendo uso de la sentencia [for...of](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Statements/for...of), y dentro del bloque `for` estamos agregando al objeto `result` una nueva llave que tendrá como valor lo que tenga `item.id` y que tiene como valor lo que tenga `item.name`.

```typescript
return result;
```

Aquí devolvemos nuestro objeto `result` después de que le agregamos las llaves y valores necesarios.

## Problema

Nuestro código funciona correctamente. Si le enviamos un arreglo de objetos con la estructura esperada obtendremos como resultado un sólo objeto.

```typescript
arrayCollectionToObject([
  { id: 'A', name: 'First' },
  { id: 'B', name: 'Second' },
  { id: 'C', name: 'Third' }
]); // { A: 'First', B: 'Second', C: 'Third' }
```

Pero hay un problema en el tipado con TypeScript, el parámetro acepta cualquier tipo variable (`any`) y el tipo de objeto retornado es simplemente un objeto vacío (`{}`).

Si a nuestra función le pasamos un argumento cualquiera, este será aceptado, TypeScript no validará nada y podremos tener errores en tiempo de ejecución.

```typescript
arrayCollectionToObject(42); // TypeError. Error en tiempo de ejecución 😭
```

Si usamos un editor con auto-completado (como [Visual Studio Code](https://code.visualstudio.com/)) no podremos aprovechar el auto-completado en el objeto devuelto por la función.

![VSCode sin auto-completado](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rcxlirkhsthgs3i3z7zw.png) 

## Mejorando el tipado de nuestra función

Tenemos como objetivo asegurar el tipo de dato que recibirá la función, permitiendo sólo colecciones de objetos que cumplan con la estructura esperada, y también debemos mejorar el tipado del objeto que devuelve la función.

### Asegurando el parámetro

Para asegurar el parámetro vamos a hacer uso de [Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html). Los Generics son una utilidad que permiten generalizar los tipos, permiten capturar el tipo provisto por el usuario para poder utilizar esta información del tipo en un futuro.

```typescript
function arrayCollectionToObject<
  T extends { id: S; name: string },
  S extends string
>(collection: T[] = []) {
  // Resto del código...
}
```

En este pequeño cambio estamos realizando lo siguiente:

```typescript
T extends { id: S; name: string }
```

Estamos diciendo que vamos a recibir un valor con un tipo determinado de dato y a este tipo lo llamaremos `T`. Lo único de lo que estamos seguros es que el tipo de dato que recibimos es un objeto y que tiene por lo menos las propiedades `id` y `name`.

La propiedad `id` tendrá otro Generic, a este tipo determinado de dato lo llamaremos `S` y nos servirá más adelante para poder agregar correctamente el tipo del resultado.

```typescript
S extends string
```

Aquí estamos agregando otra restricción a nuestro Generic llamado `S`. Estamos asegurándonos que el valor que tendrá este tipo será un sub-tipo de `string`.

Con este pequeño cambio ya estamos seguros de que nuestra función sólo recibirá como argumento un valor que cumpla con la estructura que esperamos. En caso de no cumplir con la estructura esperada, obtendremos un error en tiempo de compilación.

```typescript
arrayCollectionToObject(42); // Error en tiempo de compilación 🥳
```

### Asegurando el objeto resultante

En el paso anterior logramos asegurar el tipo del parámetro que se recibirá en la función y prevenir que se le pase como argumento cualquier tipo de valor. También podemos hacer que nuestra función nos provea un tipo más específico en el resultado que se obtiene al ejecutarla.

El objetivo es que el tipo del objeto resultante tenga como nombre de las llaves el valor que tenía cada elemento del arreglo en la llave `id`. Para lograr esto, sólo debemos hacer un cambio en la siguiente linea:

```typescript
function arrayCollectionToObject<...>(collection: T[] = []) {
  const result = {} as { [K in T['id']]: string };
  // Resto del código...
}
```

Esta linea lo que hace es que un tipo de objeto cuyas llaves serán iguales a cada uno de los valores de `id` existentes en `T` y su valor será un `string`.

¿Recuerdas que había un Generic llamado `S` en la declaración de la función? Resulta que el Generic es usado para poder tener un **String literal**, si no hubiésemos hecho esto, TypeScript nos hubiera tipado las llaves del objeto resultante como un `string` y no con el valor exacto de cada `id`.

De esta forma, ya podemos ver que el auto-completado de nuestro editor funciona correctamente.

![VSCode con auto-completado](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/njy71a8g2t0424jna8js.png)

## Código final

Después de agregar los tipos nuestro código debería haber quedado de la siguiente forma:

```typescript
function arrayCollectionToObject<
  T extends { id: S, name: string },
  S extends string
>(collection: T[] = []) {
  const result = {} as { [K in T['id']]: string };
  for (const item of collection) {
    result[item.id] = item.name;
  }
  return result;
}
```

## Conclusión

No soy un experto en TypeScript y mi experiencia con el lenguaje es poca, pero lo poco que he conocido me ha demostrado que se pueden realizar cosas muy interesantes con su sistema de tipos. Realizar este pequeño ejemplo me ayudó a fortalecer bases sobre Generics, restricción de Generics, protección de tipos y mapeado de tipos.

Es cierto que encontrar los tipos correctos en nuestro código a veces puede llevarnos muchísimo tiempo, encontrarlos para este ejercicio me llevó más tiempo del que hubiera deseado, pero se debe ver esto como una inversión a futuro. El tener nuestro código con un tipado correcto nos podrá asegurar muchísimas cosas a medida que el proyecto crece.

---

Créditos a [Mohammad Rahmani](https://unsplash.com/@afgprogrammer) por la [foto de portada](https://unsplash.com/photos/unwXUc_Pqi4) del artículo.