Como transformar um array em um objeto fortemente tipado?
Um dos problemas mais legais que já tive com tipagem no Typescript foi como transformar um array em um objeto com chave e valor tipado. Caso esteja curioso, você pode ver isso no brouther.
Para construir as rotas de configuração do Brouther, era necessário receber um array com todas as rotas. A principal ideia era mapear esse array para um objeto mapeado onde a chave é correspondente a propriedade "id" e o valor é correspondente ao "path".
Claro que não seria possível fazer essa mágica sem o uso do ts-toolbelt, uma biblioteca que entrega diversos tipos utilitários. Assim você não precisa se preocupar em construir alguns tipos super complexos do zero, e quando precisar de tipos ainda mais complexos, você tem um ferramental enorme.
Explicando da maneira mais simples possível, a ideia desse tipo é simular comportamento do .reduce
de Array para
tipos, podendo transformar o seu Array em qualquer coisa. No nosso caso, um objeto que se encaixa no
padrão Record<string, string>
. Fazer dessa forma genérica não faz sentido, pois você perde toda a inferência de tipos,
por isso a necessidade do reduce
tipado.
Antes de jogar uma tipagem super complexa aqui, é preciso explicar o uso do ts-toolbelt nesse código. Foram utilizados
dois métodos da biblioteca, F.Narrow
e N.Add
, que são basicamente são namespaces correspondentes a Function
e Number
,
respectivamente.
F.Narrow
: uma forma de garantir imutabilidade total do nosso array. Apenas o as const
não foi totalmente válido em
alguns casos, mas ele é opcional para objetos simplesN.Add
: o método de adicionar números através de tipos, você pode fazer N.Add<1,1>
e o resultado será 2
. Esse
cara é o mais importante na lógica do nosso Reduce
.Agora uma explicação da lógica necessária para chegar no resultado. Dado que você conheça o .reduce
e os índices de
arrays, será tranquilo chegar na lógica desse tipo (não dá para falar que foi fácil executar).
extends readonly any[]
para garantir a imutabilidade. O uso do any é para
determinar que pode ser qualquer tipo de array⁄K extends keyof T[number]
para sinalizar que queremos uma property expecífica do nosso array.
Essa property será usada para ser uma chave no objetoV extends keyof T[number]
para sinalizar que queremos uma property expecífica do nosso array.
Essa property será usada para ser um valor da chave no objetoC extends number = 0
para ser o nosso contador. Ele será responsável por controlar a recursividade do
nosso tipo, indo de 0 até o tamanho máximo do arrayC
seja igual ao tamanho máximo do array, retorne um objeto vazio. Como os índices do array vão de 0
até tamanho máximo - 1
essa condição encerra a recursividadeC
não seja o mesmo valor do tamanho do array, crie um objeto { readonly [_ in T[C][K]]: T[C][V] }
. Calma que
eu vou explicar. T[C]
é para pegar o item corrente do array, ou seja, para a posição C=0
, pegue o primeiro
item, C=2
pegue o segundo item e assim até que o array seja todo percorrido. T[C][K]
serve para pegar a chave do
item corrente no array, equivalente a array[0].id
. E por último T[C][V]
é responsável por pegar o valor,
equivalente a array[0].path
Reduce
, porém nesse ponto é importante lembrar que
iremos incrementar o valor de C
para podermos percorrer o array da posição 1
até N-1
.Com todos esses passos, temos esse resultado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
import type { N, F } from "ts-toolbelt"; type Reduce< T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0 > = C extends T["length"] ? {} : { readonly [_ in T[C][K]]: T[C][V] } & Reduce<T, K, V, N.Add<C, 1>> const reduce = < T extends readonly any[], K extends keyof T[number], V extends keyof T[number] >(routes: F.Narrow<T>, k: K, v: V): Reduce<T, K, V> => (routes as T).reduce((acc, el) => ({...acc, [el[k]]: el[v]}), {}) as any const map = reduce([ { id: "users", path: "/users" }, { id: "admin", path: "/admin" }, { id: "root", path: "/root" }, { id: "general", path: "/general" }, ] as const, "id", "path") // infer types from each item of your object const rootName = map.admin; const root = map.root console.log(map);
Caso tenha interesse em ver o funcionamento desse código, pode dar uma olhada no playground.