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 constnã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 = 0para 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
Cseja igual ao tamanho máximo do array, retorne um objeto vazio. Como os índices do array vão de 0 atétamanho máximo - 1essa condição encerra a recursividade - Caso
Cnã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=2pegue 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 deCpara podermos percorrer o array da posição1atéN-1.
Com todos esses passos, temos esse resultado:
import type { import NN, import FF } 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 NN.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 FF.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: anyacc, el: anyel) => ({...acc: anyacc, [el: anyel[k: K extends keyof T[number]k]]: el: anyel[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") => {
...;
} & ... 2 more ... & {
...;
}
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: Consoleconsole.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.