Criando um reduce tipado
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.
Motivação
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.
Intenção
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.
Cadê o código?
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 oas 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 fazerN.Add<1,1>
e o resultado será2
. Esse cara é o mais importante na lógica do nossoReduce
.
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).
- Precisamos receber um array readonly
extends readonly any[]
para garantir a imutabilidade. O uso do any é para determinar que pode ser qualquer tipo de array⁄ - Recebemos um generics
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 objeto - Recebemos um generics
V 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 objeto - Recebemos um
C 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 array - Caso
C
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 recursividade - Caso
C
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çãoC=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 aarray[0].id
. E por últimoT[C][V]
é responsável por pegar o valor, equivalente aarray[0].path
- Um merge é feito do objeto criado com um objeto recursivo de
Reduce
, porém nesse ponto é importante lembrar que iremos incrementar o valor deC
para podermos percorrer o array da posição1
atéN-1
.
Com todos esses passos, temos esse resultado:
import type { import N
N, import F
F } from "ts-toolbelt";
type 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>>
Reduce<
function (type parameter) T in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
T extends readonly any[],
function (type parameter) K in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
K extends keyof function (type parameter) T in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
T[number],
function (type parameter) V in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
V extends keyof function (type parameter) T in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
T[number],
function (type parameter) C in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
C extends number = 0
> =
function (type parameter) C in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
C extends function (type parameter) T in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
T["length"] ? {} : {
readonly [function (type parameter) _
_ in function (type parameter) T in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
T[function (type parameter) C in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
C][function (type parameter) K in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
K]]: function (type parameter) T in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
T[function (type parameter) C in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
C][function (type parameter) V in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
V]
} & 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>>
Reduce<function (type parameter) T in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
T, function (type parameter) K in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
K, function (type parameter) V in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
V, import N
N.type Add<N1 extends number, N2 extends number> = N1 extends unknown ? N2 extends unknown ? _Add<IterationOf<N1>, IterationOf<N2>>[0] : never : never
export Add
Add a [[Number]] to another oneAdd<function (type parameter) C in type Reduce<T extends readonly any[], K extends keyof T[number], V extends keyof T[number], C extends number = 0>
C, 1>>
const 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>
reduce =
<
function (type parameter) T in <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>
T extends readonly any[],
function (type parameter) K in <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>
K extends keyof function (type parameter) T in <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>
T[number],
function (type parameter) V in <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>
V extends keyof function (type parameter) T in <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>
T[number]
>(routes: F.Narrow<T>
routes: import F
F.type Narrow<A extends unknown> = A extends [] ? A : NarrowRaw<A>
export Narrow
Prevent type widening on generic function parametersNarrow<function (type parameter) T in <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>
T>, k: K extends keyof T[number]
k: function (type parameter) K in <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>
K, v: V extends keyof T[number]
v: function (type parameter) V in <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>
V):
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>>
Reduce<function (type parameter) T in <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>
T, function (type parameter) K in <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>
K, function (type parameter) V in <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>
V> =>
(routes: F.Narrow<T>
routes as function (type parameter) T in <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>
T).ReadonlyArray<any>.reduce(callbackfn: (previousValue: any, currentValue: any, currentIndex: number, array: readonly any[]) => any, initialValue: any): any (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.reduce((acc: any
acc, el: any
el) => ({...acc: any
acc, [el: any
el[k: K extends keyof T[number]
k]]: el: any
el[v: V extends keyof T[number]
v]}), {}) as any
const const map: {
readonly users: "/users";
} & {
readonly admin: "/admin";
} & {
readonly root: "/root";
} & {
readonly general: "/general";
}
map = const reduce: <[{
readonly id: "users";
readonly path: "/users";
}, {
readonly id: "admin";
readonly path: "/admin";
}, {
readonly id: "root";
readonly path: "/root";
}, {
readonly id: "general";
readonly path: "/general";
}], "id", "path">(routes: [...], k: "id", v: "path") => Reduce<[...], "id", "path">
reduce([
{ id: "users"
id: "users", path: "/users"
path: "/users" },
{ id: "admin"
id: "admin", path: "/admin"
path: "/admin" },
{ id: "root"
id: "root", path: "/root"
path: "/root" },
{ id: "general"
id: "general", path: "/general"
path: "/general" },
] as type const = [{
readonly id: "users";
readonly path: "/users";
}, {
readonly id: "admin";
readonly path: "/admin";
}, {
readonly id: "root";
readonly path: "/root";
}, {
readonly id: "general";
readonly path: "/general";
}]
const, "id", "path")
// infer types from each item of your object
const const rootName: "/admin"
rootName = const map: {
readonly users: "/users";
} & {
readonly admin: "/admin";
} & {
readonly root: "/root";
} & {
readonly general: "/general";
}
map.admin: "/admin"
admin;
const const root: "/root"
root = const map: {
readonly users: "/users";
} & {
readonly admin: "/admin";
} & {
readonly root: "/root";
} & {
readonly general: "/general";
}
map.root: "/root"
root
var console: Console
console.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(const map: {
readonly users: "/users";
} & {
readonly admin: "/admin";
} & {
readonly root: "/root";
} & {
readonly general: "/general";
}
map);
Caso tenha interesse em ver o funcionamento desse código, pode dar uma olhada no playground.