Kaique Mitsuo Silva Yamamoto
Arquitetura softwareFrontend webTypeScriptProgramação Funcional

Composição e Currying em TypeScript

pipe, 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 — confuso

pipe — 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);   // 8

Por 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

Referências

On this page