Tipando funções de alta ordem e currying

1. Fundamentos: Funções de alta ordem e seus tipos

Funções de alta ordem (HOFs) são funções que recebem outras funções como argumento ou retornam funções como resultado. No TypeScript, a tipagem dessas funções é essencial para manter a segurança de tipos.

A tipagem básica de uma HOF segue o padrão (fn: (x: T) => U) => V. Vamos implementar manualmente versões tipadas de funções clássicas como map, filter e reduce:

// Map manualmente tipado
function meuMap<T, U>(arr: T[], fn: (item: T, index: number) => U): U[] {
  const resultado: U[] = [];
  for (let i = 0; i < arr.length; i++) {
    resultado.push(fn(arr[i], i));
  }
  return resultado;
}

// Filter manualmente tipado
function meuFilter<T>(arr: T[], predicate: (item: T, index: number) => boolean): T[] {
  const resultado: T[] = [];
  for (let i = 0; i < arr.length; i++) {
    if (predicate(arr[i], i)) {
      resultado.push(arr[i]);
    }
  }
  return resultado;
}

// Reduce manualmente tipado
function meuReduce<T, U>(
  arr: T[],
  reducer: (acumulador: U, item: T, index: number) => U,
  valorInicial: U
): U {
  let acumulador = valorInicial;
  for (let i = 0; i < arr.length; i++) {
    acumulador = reducer(acumulador, arr[i], i);
  }
  return acumulador;
}

// Exemplo de uso
const numeros = [1, 2, 3, 4, 5];
const dobrados = meuMap(numeros, (n) => n * 2); // type: number[]
const pares = meuFilter(numeros, (n) => n % 2 === 0); // type: number[]
const soma = meuReduce(numeros, (acc, n) => acc + n, 0); // type: number

2. Inferência de tipos em HOFs com genéricos

O TypeScript infere automaticamente os tipos dos genéricos quando usamos HOFs. No entanto, existem armadilhas que podem causar perda de tipo, especialmente em callbacks aninhadas:

// Inferência automática funciona bem
function aplicarOperacao<T, U>(valor: T, fn: (x: T) => U): U {
  return fn(valor);
}

const resultado = aplicarOperacao(10, (x) => x.toString());
// resultado é inferido como string automaticamente

// Armadilha: perda de tipo em callbacks aninhadas
function processarArray<T, U>(
  arr: T[],
  transformador: (item: T) => U
): U[] {
  return arr.map(transformador);
}

// Problema comum: quando o callback retorna um objeto complexo
const usuarios = [{ nome: "João", idade: 30 }, { nome: "Maria", idade: 25 }];
const nomes = processarArray(usuarios, (usuario) => usuario.nome);
// nomes é inferido como string[] corretamente

// Explicitação de tipos quando necessário
const idades = processarArray<typeof usuarios[0], number>(
  usuarios,
  (usuario) => usuario.idade
);

3. Currying: conceito e tipagem inicial

Currying é a técnica de transformar uma função que recebe múltiplos argumentos em uma sequência de funções que recebem um argumento cada. A tipagem básica segue o padrão (a: A) => (b: B) => C:

// Função curried simples
function somarCurried(a: number): (b: number) => number {
  return (b: number) => a + b;
}

const somarCom5 = somarCurried(5);
console.log(somarCom5(3)); // 8

// Currying com três parâmetros
function formatarMensagem(
  prefixo: string
): (sufixo: string) => (mensagem: string) => string {
  return (sufixo: string) => (mensagem: string) => 
    `${prefixo} ${mensagem} ${sufixo}`;
}

const comPrefixo = formatarMensagem("[INFO]");
const comPrefixoESufixo = comPrefixo("[OK]");
console.log(comPrefixoESufixo("Sistema iniciado"));
// "[INFO] Sistema iniciado [OK]"

// Desafio: currying com aridade variável
function curriedAdd(a: number): (b: number) => number;
function curriedAdd(a: number, b: number): number;
function curriedAdd(a: number, b?: number): unknown {
  if (b === undefined) {
    return (c: number) => a + c;
  }
  return a + b;
}

4. Tipagem avançada de currying com genéricos condicionais

Para criar uma função curry genérica, usamos infer e tipos condicionais para extrair parâmetros automaticamente:

// Tipo condicional para extrair parâmetros
type Parametros<T> = T extends (...args: infer P) => unknown ? P : never;
type Retorno<T> = T extends (...args: unknown[]) => infer R ? R : never;

// Tipo Curried que transforma qualquer função em curried
type Curried<T> = 
  T extends (a: infer A) => infer R
    ? (a: A) => R
    : T extends (a: infer A, b: infer B, ...rest: infer C) => infer R
      ? (a: A) => Curried<(...args: [B, ...C]) => R>
      : never;

// Implementação genérica
function curry<T extends (...args: unknown[]) => unknown>(
  fn: T
): Curried<T> {
  return function curried(...args: unknown[]) {
    if (args.length >= fn.length) {
      return fn(...args);
    }
    return (...args2: unknown[]) => curried(...args, ...args2);
  } as Curried<T>;
}

// Exemplo de uso
function somar(a: number, b: number, c: number): number {
  return a + b + c;
}

const somarCurriedGen = curry(somar);
const resultadoParcial = somarCurriedGen(1)(2); // Retorna função
const resultadoFinal = somarCurriedGen(1)(2)(3); // 6

5. Sobrecarga e união de tipos em HOFs curried

Sobrecarga de funções permite múltiplas assinaturas para diferentes aridades. União de tipos em parâmetros amplia as possibilidades:

// Sobrecarga para múltiplas aridades
function processarDados(
  fn: (a: string) => number
): (dado: string) => number;
function processarDados(
  fn: (a: string, b: string) => number
): (dado1: string) => (dado2: string) => number;
function processarDados(fn: (...args: string[]) => number): unknown {
  if (fn.length === 1) {
    return (dado: string) => fn(dado);
  }
  return (dado1: string) => (dado2: string) => fn(dado1, dado2);
}

// União de tipos em parâmetros
function transformar<T extends string | number>(
  valor: T
): T extends string ? string : number {
  return typeof valor === "string" 
    ? valor.toUpperCase() as any 
    : valor * 2 as any;
}

// Exemplo similar ao lodash _.curry
type FuncaoQualquer = (...args: unknown[]) => unknown;

function curryOverload<T extends FuncaoQualquer>(
  fn: T
): Curried<T> & { placeholder: symbol } {
  const curried = curry(fn);
  return Object.assign(curried, { placeholder: Symbol("_") });
}

6. HOFs com estado e closures tipados

Closures retornadas por HOFs precisam de tipagem cuidadosa para preservar o estado interno:

// Memoize tipado com cache genérico
function memoize<T extends (...args: unknown[]) => unknown>(
  fn: T
): (...args: Parameters<T>) => ReturnType<T> {
  const cache = new Map<string, ReturnType<T>>();

  return (...args: Parameters<T>): ReturnType<T> => {
    const chave = JSON.stringify(args);
    if (cache.has(chave)) {
      return cache.get(chave)!;
    }
    const resultado = fn(...args) as ReturnType<T>;
    cache.set(chave, resultado);
    return resultado;
  };
}

// Exemplo de uso com estado
function criarContador(valorInicial: number): {
  incrementar: () => number;
  decrementar: () => number;
  valor: () => number;
} {
  let contador = valorInicial;

  return {
    incrementar: () => ++contador,
    decrementar: () => --contador,
    valor: () => contador
  };
}

// HOF assíncrona tipada
async function comTimeout<T>(
  fn: () => Promise<T>,
  ms: number
): Promise<T> {
  return Promise.race([
    fn(),
    new Promise<T>((_, reject) => 
      setTimeout(() => reject(new Error("Timeout")), ms)
    )
  ]);
}

7. Padrões práticos e boas práticas

Utilize utilitários de tipo como Parameters<T> e ReturnType<T> para criar HOFs mais flexíveis:

// Uso de Parameters e ReturnType
function wrapper<T extends (...args: unknown[]) => unknown>(
  fn: T
): (...args: Parameters<T>) => ReturnType<T> {
  return (...args: Parameters<T>): ReturnType<T> => {
    console.log(`Chamando ${fn.name} com`, args);
    const resultado = fn(...args);
    console.log(`Resultado:`, resultado);
    return resultado;
  };
}

// Evitando any e Function
// ❌ Ruim
function ruimHOF(fn: Function): any {
  return fn();
}

// ✅ Bom
function boaHOF<T extends (...args: unknown[]) => unknown>(
  fn: T
): (...args: Parameters<T>) => ReturnType<T> {
  return (...args) => fn(...args) as ReturnType<T>;
}

// Testes de tipo com função auxiliar
function expectType<T>(value: T): void {}

const numerosDobrados = meuMap([1, 2, 3], (n) => n * 2);
expectType<number[]>(numerosDobrados); // Verifica o tipo

8. Casos reais: bibliotecas e frameworks

Bibliotecas populares utilizam extensivamente HOFs e currying com tipagem avançada:

// Redux: connect e compose tipados
import { connect, compose } from "react-redux";

interface Estado {
  usuario: { nome: string };
}

const mapStateToProps = (state: Estado) => ({
  nome: state.usuario.nome,
});

const mapDispatchToProps = {
  atualizarNome: (nome: string) => ({ type: "ATUALIZAR_NOME", payload: nome }),
};

// connect é uma HOF que retorna um componente tipado
const ComponenteConectado = connect(mapStateToProps, mapDispatchToProps);

// compose combina múltiplas HOFs
const funcaoComposta = compose(
  (fn: (x: number) => number) => (x: number) => fn(x) * 2,
  (fn: (x: number) => number) => (x: number) => fn(x) + 1
);

// Zod: currying em validação
import { z } from "zod";

const esquemaUsuario = z.object({
  nome: z.string().min(3),
  idade: z.number().min(18),
});

// RxJS: pipe e operadores tipados
import { of, pipe } from "rxjs";
import { map, filter } from "rxjs/operators";

const observable = of(1, 2, 3, 4, 5).pipe(
  filter((x) => x % 2 === 0),
  map((x) => x * 10)
);

Referências