DEV Community

Cover image for Convertir un array de objetos a un objeto usando TypeScript
David Sánchez
David Sánchez

Posted on

Convertir un array de objetos a un objeto usando TypeScript

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:

arrayCollectionToObject([
  { id: 'A', name: 'First' },
  { id: 'B', name: 'Second' },
  { id: 'C', name: 'Third' }
]); // { A: 'First', B: 'Second', C: 'Third' }
Enter fullscreen mode Exit fullscreen mode

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í:

function arrayCollectionToObject(collection) {
  const result = {};
  for (const item of collection) {
    result[item.id] = item.name;
  }
  return result;
}
Enter fullscreen mode Exit fullscreen mode

Nota: Para disminuir un poco la complejidad del código en el ejemplo he usado una aproximación imperativa en lugar de usar una aproximación declarativa haciendo uso del reduce.

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

const result = {};
Enter fullscreen mode Exit fullscreen mode

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.

for (const item of collection) {
  result[item.id] = item.name;
}
Enter fullscreen mode Exit fullscreen mode

Aquí estamos iterando uno a uno los elementos que están en el array, haciendo uso de la sentencia 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.

return result;
Enter fullscreen mode Exit fullscreen mode

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.

arrayCollectionToObject([
  { id: 'A', name: 'First' },
  { id: 'B', name: 'Second' },
  { id: 'C', name: 'Third' }
]); // { A: 'First', B: 'Second', C: 'Third' }
Enter fullscreen mode Exit fullscreen mode

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.

arrayCollectionToObject(42); // TypeError. Error en tiempo de ejecución 😭
Enter fullscreen mode Exit fullscreen mode

Si usamos un editor con auto-completado (como Visual Studio Code) no podremos aprovechar el auto-completado en el objeto devuelto por la función.

VSCode sin auto-completado

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

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

En este pequeño cambio estamos realizando lo siguiente:

T extends { id: S; name: string }
Enter fullscreen mode Exit fullscreen mode

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.

S extends string
Enter fullscreen mode Exit fullscreen mode

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.

arrayCollectionToObject(42); // Error en tiempo de compilación 🥳
Enter fullscreen mode Exit fullscreen mode

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:

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

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

Código final

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

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;
}
Enter fullscreen mode Exit fullscreen mode

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 por la foto de portada del artículo.

Top comments (1)

Collapse
 
driftinglive profile image
Drifting Live

👏👏👏