Arquitetura softwareFrontend webTypeScriptProgramação Funcional
Composição e Currying em TypeScript
Baixar PDFpipe, flow, currying, partial application — como construir funções complexas a partir de funções simples.
Composição e Currying em TypeScript
Composição é o princípio mais poderoso da programação funcional: construir funções complexas combinando funções simples. É como Lego — cada peça faz uma coisa, e você monta o que precisa.
Composição de funções
O problema: aninhamento
// ❌ Aninhamento — difícil de ler, ordem invertida
const result = format(
calculate(
filter(
sort(items)
)
)
);
// A ordem de execução é DE DENTRO PARA FORA — confusopipe — Ler da esquerda para direita
// Implementação do pipe (simplificada)
function pipe<A>(value: A): A;
function pipe<A, B>(value: A, fn1: (a: A) => B): B;
function pipe<A, B, C>(value: A, fn1: (a: A) => B, fn2: (b: B) => C): C;
function pipe<A, B, C, D>(value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D): D;
function pipe(value: any, ...fns: Function[]): any {
return fns.reduce((acc, fn) => fn(acc), value);
}
// ✅ Pipe — lê como um texto, da esquerda para direita
const result = pipe(
items,
sort, // 1. ordena
filter, // 2. filtra
calculate, // 3. calcula
format, // 4. formata
);Exemplo real — Processamento de pedido
// Funções pequenas e puras — cada uma faz UMA coisa
type Product = { name: string; price: number; category: string };
type Cart = { items: Product[]; total: number };
const filterInStock = (products: Product[]): Product[] =>
products.filter(p => p.price > 0);
const applyDiscount = (percent: number) => (products: Product[]): Product[] =>
products.map(p => ({ ...p, price: p.price * (1 - percent / 100) }));
const sortByPrice = (products: Product[]): Product[] =>
[...products].sort((a, b) => a.price - b.price);
const calculateTotal = (products: Product[]): Cart => ({
items: products,
total: products.reduce((sum, p) => sum + p.price, 0),
});
const formatCart = (cart: Cart): string =>
`${cart.items.length} itens — Total: R$ ${cart.total.toFixed(2)}`;
// Composição — legível, cada passo claro
const processCart = (products: Product[]) =>
pipe(
products,
filterInStock,
applyDiscount(10),
sortByPrice,
calculateTotal,
formatCart,
);
// Saída: "3 itens — Total: R$ 135.00"Frontend — Pipe para dados de tabela
type User = { name: string; email: string; age: number; active: boolean };
const filterActive = (users: User[]): User[] =>
users.filter(u => u.active);
const sortByAge = (users: User[]): User[] =>
[...users].sort((a, b) => a.age - b.age);
const toDisplayRows = (users: User[]): DisplayRow[] =>
users.map(u => ({
label: `${u.name} (${u.age})`,
subtitle: u.email,
}));
const paginate = (page: number, perPage: number) => <T>(items: T[]): T[] =>
items.slice((page - 1) * perPage, page * perPage);
// Uso
const tableData = pipe(
allUsers,
filterActive,
sortByAge,
toDisplayRows,
paginate(currentPage, 10),
);Backend — Pipe para validação
type ValidationResult<T> = { valid: true; value: T } | { valid: false; errors: string[] };
const validateEmail = (email: string): ValidationResult<string> =>
email.includes('@')
? { valid: true, value: email }
: { valid: false, errors: ['Email inválido'] };
const validateLength = (min: number, max: number) => (value: string): ValidationResult<string> =>
value.length >= min && value.length <= max
? { valid: true, value }
: { valid: false, errors: [`Deve ter entre ${min} e ${max} caracteres`] };
const validateNotDisposable = (email: string): ValidationResult<string> =>
!email.includes('tempmail')
? { valid: true, value: email }
: { valid: false, errors: ['Email descartável não permitido'] };
// Pipe com validações — para na primeira falha
function validate<T>(
value: T,
...validators: Array<(v: T) => ValidationResult<T>>
): ValidationResult<T> {
for (const validator of validators) {
const result = validator(value);
if (!result.valid) return result;
}
return { valid: true, value };
}
const result = validate(
'[email protected]',
validateEmail,
validateLength(5, 100),
validateNotDisposable,
);Currying — Uma função com múltiplos argumentos virando múltiplas funções com um argumento
Conceito
// Normal — todos os argumentos de uma vez
function add(a: number, b: number): number {
return a + b;
}
// Curried — um argumento por vez
function add(a: number): (b: number) => number {
return (b) => a + b;
}
const add5 = add(5); // (b: number) => number
const result = add5(3); // 8Por que currying é útil?
Permite especializar funções genéricas:
// Função genérica
const multiply = (a: number) => (b: number) => a * b;
// Especializações
const double = multiply(2);
const triple = multiply(3);
const toPercent = multiply(100);
[1, 2, 3].map(double); // [2, 4, 6]
[1, 2, 3].map(triple); // [3, 6, 9]
[0.5, 0.75].map(toPercent); // [50, 75]Frontend — Validadores curried
const validateMinLength = (min: number) => (value: string): string | null =>
value.length < min ? `Mínimo de ${min} caracteres` : null;
const validateMaxLength = (max: number) => (value: string): string | null =>
value.length > max ? `Máximo de ${max} caracteres` : null;
const validatePattern = (pattern: RegExp, message: string) => (value: string): string | null =>
pattern.test(value) ? null : message;
// Especializações
const validateName = [
validateMinLength(2),
validateMaxLength(100),
];
const validateEmail = [
validateMinLength(5),
validatePattern(/@/, 'Deve conter @'),
validatePattern(/\.com$|\.br$/, 'Deve terminar com .com ou .br'),
];
// Uso no form
function runValidators(value: string, validators: Array<(v: string) => string | null>): string[] {
return validators.map(v => v(value)).filter((e): e is string => e !== null);
}
runValidators('[email protected]', validateEmail); // []
runValidators('abc', validateEmail); // ['Deve conter @', 'Deve terminar com .com ou .br']Backend — Query builders curried
// Função curried para criar queries
const findWhere = (collection: string) => (field: string) => (value: unknown) =>
db.collection(collection).find({ [field]: value });
// Especializações
const findUsers = findWhere('users');
const findUserByEmail = findUsers('email');
const findUserById = findUsers('_id');
// Uso
await findUserByEmail('[email protected]');
await findUserById('64a1b2c3d4e5f6a7b8c9d0e1');Partial application — Preencher alguns argumentos
Similar ao currying, mas você pode preencher qualquer argumento, não apenas o primeiro:
function partial<T extends any[], R>(
fn: (...args: T) => R,
...partialArgs: Partial<T>
): (...remaining: any[]) => R {
return (...remaining: any[]) => fn(...partialArgs, ...remaining);
}
// Função original com 3 argumentos
function log(level: string, timestamp: Date, message: string) {
console.log(`[${level}] ${timestamp.toISOString()} — ${message}`);
}
// Preenche os dois primeiros
const info = partial(log, 'INFO', new Date());
const error = partial(log, 'ERROR', new Date());
// Uso
info('Usuário logado'); // [INFO] 2025-01-15T10:30:00Z — Usuário logado
error('Falha na conexão'); // [ERROR] 2025-01-15T10:30:00Z — Falha na conexão