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 o as const não foi totalmente válido em alguns casos, mas ele é opcional para objetos simples
  • N.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).

  1. 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⁄
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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çã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
  7. Um merge é feito do objeto criado com um objeto recursivo de 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:

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 one
@paramN1 Left-hand side@paramN2 Right-hand side@returns`string | number | boolean`@example```ts import {N} from 'ts-toolbelt' type test0 = N.Add<'2', '10'> // '12' type test1 = N.Add<'0', '40'> // '40' type test2 = N.Add<'0', '40', 's'> // '40' type test3 = N.Add<'0', '40', 'n'> // 40 type test4 = N.Add<'-20', '40', 's'> // '20' type test5 = N.Add<'-20', '40', 'n'> // 20 ```
Add
<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 parameters
@paramA to narrow@returns`A`@example```ts import {F} from 'ts-toolbelt' declare function foo<A extends any[]>(x: F.Narrow<A>): A; declare function bar<A extends object>(x: F.Narrow<A>): A; const test0 = foo(['e', 2, true, {f: ['g', ['h']]}]) // `A` inferred : ['e', 2, true, {f: ['g']}] const test1 = bar({a: 1, b: 'c', d: ['e', 2, true, {f: ['g']}]}) // `A` inferred : {a: 1, b: 'c', d: ['e', 2, true, {f: ['g']}]} ```
Narrow
<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.
@paramcallbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.@paraminitialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.
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") => 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: 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.