Programação Funcional em TypeScript: nível avançado
Programação Funcional (PF) é um paradigma que, nos últimos anos, vem conquistando ativamente o mundo do JavaScript e TypeScript. Para desenvolvedores já familiarizados com os fundamentos (funções puras, imutabilidade de dados), abre-se uma nova camada de possibilidades: trabalhar com mônadas, functores, aplicativos e composições complexas. Neste artigo, vamos analisar conceitos avançados de PF em TypeScript com exemplos práticos.
1. Imutabilidade e compartilhamento estrutural de dados
Imutabilidade é a pedra angular da PF. Em TypeScript, podemos garantir a imutabilidade no nível de tipos usando readonly e ReadonlyArray. No entanto, para trabalhar eficientemente com grandes estruturas de dados (por exemplo, Redux store), utiliza-se o compartilhamento estrutural (structural sharing) — bibliotecas como Immer ou Immutable.js.
Vamos considerar um exemplo usando readonly e uma função para atualizar um objeto aninhado sem mutação:
type User = { readonly id: number; readonly name: string; readonly address: { readonly city: string; readonly zip: string; };};
const updateUserCity = (user: User, newCity: string): User => ({ ...user, address: { ...user.address, city: newCity, },});
const user: User = { id: 1, name: 'Alice', address: { city: 'NYC', zip: '10001' } };const updatedUser = updateUserCity(user, 'LA');console.log(user.address.city); // NYC (original não foi alterado)console.log(updatedUser.address.city); // LAObserve: criamos um novo objeto, mas reutilizamos as partes não modificadas (por exemplo, id e name). Isso é o compartilhamento estrutural manual. Para aninhamento profundo, é melhor usar bibliotecas.
2. Functores e sua implementação
Functor é um contêiner que implementa o método map, permitindo aplicar uma função ao conteúdo do contêiner sem violar sua estrutura. Em TypeScript, um functor típico é Array. Mas podemos criar nosso próprio functor, por exemplo, Maybe, para evitar erros com null e undefined.
Vamos implementar um functor Maybe simples:
class Maybe<T> { private constructor(private value: T | null) {}
static of<U>(val: U | null | undefined): Maybe<U> { return new Maybe(val ?? null); }
map<U>(fn: (val: T) => U): Maybe<U> { if (this.value === null) { return Maybe.of<U>(null); } return Maybe.of(fn(this.value)); }
getOrElse(defaultValue: T): T { return this.value !== null ? this.value : defaultValue; }}
// Usoconst result = Maybe.of('hello') .map(s => s.toUpperCase()) .map(s => s + ' world') .getOrElse('fallback');console.log(result); // "HELLO WORLD"
const empty = Maybe.of<string>(null) .map(s => s.toUpperCase()) .getOrElse('default');console.log(empty); // "default"O functor Maybe permite encadear chamadas de funções com segurança, sem se preocupar com null. Cada map retorna um novo Maybe, preservando o contexto.
3. Mônadas: cadeias de operações com efeitos colaterais
Mônada é um functor que também implementa o método flatMap (frequentemente chamado de bind ou chain). Ele permite "desembrulhar" contêineres aninhados. Um exemplo clássico de mônada é Promise ou Array. Vamos considerar a mônada Either para tratamento de erros sem exceções.
type Either<L, R> = Left<L> | Right<R>;
class Left<L> { readonly value: L; constructor(value: L) { this.value = value; } isLeft(): this is Left<L> { return true; } isRight(): this is Right<ne