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
- TypeScript Handbook: Functions — Documentação oficial sobre funções, incluindo genéricos e inferência de tipos
- TypeScript Handbook: Conditional Types — Guia completo sobre tipos condicionais e operador
infer - TypeScript Deep Dive: Currying — Explicação detalhada sobre implementação de currying em TypeScript
- Zod Documentation: Type Inference — Documentação oficial sobre inferência de tipos em esquemas de validação
- RxJS Documentation: Pipeable Operators — Guia sobre operadores pipeable e sua tipagem no RxJS
- Redux Documentation: TypeScript Usage — Guia oficial sobre uso de TypeScript com Redux, incluindo HOFs como
connectecompose - TypeScript Generics and Higher-Order Functions — Exemplos interativos no Playground do TypeScript sobre funções de alta ordem